forked from unom/punktfunk
Completes WP-C1 of design/host-source-stutter-fixes.md. The three Rust clients conceal a packet drought on their decode thread; Apple could not, and the reason is structural rather than an oversight: its playout ring is Swift and its Opus decoder lives behind the C ABI, so the drain thread had nothing to call. Standing up a second decoder Swift-side would not have worked either — PLC extrapolates from the LAST DECODED FRAME, so a fresh decoder conceals from empty state. So the ABI grows one function. `punktfunk_connection_audio_plc` synthesizes a single frame from the connection's own live decoder, returning no-frame when nothing has decoded yet (there is no state to extrapolate from) and when libopus declines to interpolate — a timeout's answer, not an error. Frames it returns carry seq and pts of zero, because concealed audio was never on the wire and must not reach an A/V-sync observation; the drain loop skips `av.observe` for them. The double-conceal defence had to move. On the Rust clients the decode loop subtracts frames it already concealed from what AudioGapTracker then asks for; Swift cannot, because the tracker is behind the ABI. So `AudioPcmState` counts its own concealment and `decode_packet` does the subtraction — same invariant, enforced one layer lower, with a Rust test standing in for the Swift half. Swift gets the policy port beside `AudioRing`'s existing one, in the same shape and for the same reason: one budget, `deprimeMS * 2`, denominated in time and not in callbacks. `plc_ms=` joins the 10 s line as it did on the other three. ABI_VERSION 22 → 23. ⚠ That number is the one thing here worth a second look: 21 was claimed twice by concurrent worktrees once already, and 23 is correct only against what is visible from this branch.