fix(audio): rebuild the resampler when a frame's input format changes mid-stream - #453
Merged
superuser404notfound merged 1 commit intoAug 30, 2026
Conversation
… mid-stream A live splice that swaps the stream's audio configuration (5.1 <-> stereo at a program boundary) leaves the SwrContext configured for the first frame's layout; a context built for more input planes than the frame carries reads a NULL plane inside swr_convert and crashes the demux thread (superuser404notfound#452). Record the input parameters the context was built for and compare each received frame against them in decode() and drain(). On a change, emit the accumulator first (its bytes are in the old output format), reset the gapless clock (its sample count is denominated in the old rate), and rebuild the resampler and format description from the new frame. A rate or format switch that previously resampled garbage silently (an HE-AAC first frame at the core rate, say) now rebuilds too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HewBbYmNEut2WvoEHmeBRU
superuser404notfound
merged commit Aug 30, 2026
654c69e
into
superuser404notfound:main
4 checks passed
superuser404notfound
added a commit
that referenced
this pull request
Aug 30, 2026
…very site that keeps one #453 closed the crash on the SW path: a live TS splice from 5.1 to stereo left AudioDecoder's SwrContext reading six planes from a frame that carries two, and swr_convert faulted on the first NULL one. This carries that rule to the rest of the surface it belongs to. The compare and the plane check now live in one place, ResamplerInputParameters, with unit tests around the cases the crash walked through: the reported splice, a rate change, a format change, a layout change at the same channel count, and a frame that states no layout (which must not rebuild the synthesised default on every frame). A layout comparison that cannot decide counts as changed, matching AudioBridge, because carrying on with a stale context is the fault being fixed. AudioTapDecoder had the same gap and is fed from the same live sources. Its output is pinned to mono 48 kHz, so only the input can move and nothing has to be flushed across the seam; it now rebuilds on the same signal. Both decoders also refuse a frame that is missing a plane the context would read. The compare covers a DECLARED change; corrupt live MPEG-TS decodes to nb_samples > 0 with a NULL plane and declares nothing, which is the same dereference by another route. AudioBridge already guarded that case on the loopback path. Full suite green (2382 tests, 606 XCTest). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WbciAqCSxpaiUpTumrCuA9
Owner
|
Merged as Reviewed and run against the full suite here before the merge (2374 tests, green), then hardened in
Full suite green after the follow-up (2382 tests). A release carrying both goes out shortly. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #452.
AudioDecoderbuilds itsSwrContextlazily from the first decoded frame and never revisits it. A live splice that swaps the stream's audio configuration mid-session (5.1 ↔ stereo at a program boundary is routine on European broadcast TS) leaves the context configured for the first frame's layout; a context built for more input planes than the frame carries reads a NULL plane insideswr_convertand crashes the demux thread. Full fault analysis (disassembly, registers, KSCrash capture) is in the issue.The fix: record the input parameters the context was built for (
ch_layoutcopy,format, rate) and compare each received frame against them indecode()anddrain(). On a change:emitPending()first — the accumulator's bytes are in the OLD output format and must be neither dropped nor mixed with the new format's;swr_free+ re-run the existinginitResamplerFromFrame(_:), which also rebuilds theCMAudioFormatDescriptionso the renderer sees the new ASBD on subsequent buffers;AudioClockAnchor— its emitted-sample count is denominated in the old rate; the next buffer re-anchors from its own container PTS.A failed rebuild leaves
swrContextnil and skips the frame; the next frame retries, mirroring the lazy-init failure mode. The layout compare only runs when the frame carries a valid layout, so an UNSPEC stream that fell back to the synthesised default at init does not rebuild per frame. A rate/format switch that previously resampled garbage silently (an HE-AAC first frame at the core rate, say) now rebuilds too.Field-verified on device (Apple TV 4K 3rd gen, tvOS 26.6, this branch as a local override on 6.56.5): tuned the crashing channel, hit a real on-air AC-3 5.1 → stereo splice two minutes in —
One detection, no flapping; the SW diag line holds
dclk=1.00 / status=renderingstraight through the seam, playback continues cleanly. On 6.56.5 this exact moment is the crash from the issue.🤖 Generated with Claude Code
https://claude.ai/code/session_01HewBbYmNEut2WvoEHmeBRU