Skip to content

fix(akouo-core): decode >2-channel Opus via multistream instead of collapsing to stereo (#544) - #571

Merged
forkwright merged 1 commit into
mainfrom
audit/opus
Jul 6, 2026
Merged

fix(akouo-core): decode >2-channel Opus via multistream instead of collapsing to stereo (#544)#571
forkwright merged 1 commit into
mainfrom
audit/opus

Conversation

@forkwright

Copy link
Copy Markdown
Owner

Verified decode-correctness fix from the 2026-07-03 deep-audit workflow (adversarially verified + Opus-judged).

#544 — >2-channel Opus streams were collapsed to stereo. opus_channels was forced to Mono/Stereo at both decoder construction and post-seek reset, while total = n_samples × channels used the source's TRUE (possibly >2) channel count. A >2-channel Opus stream therefore either errored on every packet (libopus configured stereo) or fabricated extra-channel audio from stale decode_buf bytes, desyncing every downstream per-channel DSP stage — a silently-wrong-audio bug.

Fix — decode multichannel correctly (opusic-c exposes a multistream decoder). Verified against the installed crate source: opusic_c::multistream::Decoder (libopus OpusMSDecoder) and multistream::Config<CH> (RFC-7845 stream/coupled/mapping validation) are available. The fix resolves the real channel layout from the OpusHead identification header (which Symphonia forwards via AudioCodecParameters.extra_data for OGG/MP4-dOps/Matroska):

  • mono/stereo family-0 streams keep the existing opusic_c::Decoder;
  • mapping-family-1 streams (≤8 channels) decode through multistream::Decoder with the correct channel-mapping table;
  • genuinely-undecodable layouts — reserved mapping families, family-0 with >2 channels, family-1 >8ch, and >2 channels with no OpusHead (no mapping table exists) — return DecodeError::UnsupportedCodec (the WavPack idiom) instead of collapsing.

The seek path now resets decoder state in place (OPUS_RESET_STATE) instead of rebuilding, removing the duplicated collapse logic. channels flows from the resolved layout's true count, with an INVARIANT tag tying it to the interleaved-sample math.

Note for reviewers: adds one unsafe impl Send for MultiDecoderopusic-c marks the structurally-identical Decoder/Encoder/multistream::Encoder as Send but omits it on multistream::Decoder; the decoder is exclusively-owned heap state (&mut self-only, no thread affinity), so the transfer is sound. parse_opus_head is bounds-checked on all offsets and never panics on malformed extra_data.

Gate: kanon gate --full green — fmt, check, clippy (-D warnings --all-targets), nextest 1923 passed, deny, lint (0 errors). 13 opus-module tests (7 pre-existing + 6 new), including a real libopus 5.1 encode→decode round-trip proving six channels decode a full frame each. Gate-Passed trailer stamped.

Closes #544

Gate-Passed: kanon 0.1.5 +stages:fmt,check,clippy,nextest,lint sha:9a7a3585a10b70ae86196b4a5f2fa0d2ee80485c
@forkwright
forkwright merged commit 809d301 into main Jul 6, 2026
12 checks passed
@forkwright
forkwright deleted the audit/opus branch July 6, 2026 22:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

decode correctness: Opus >2-channel streams collapsed to stereo

1 participant