fix(client/audio): the in-core decoder conceals lost packets like every other client
A field report: game audio on a MacBook (M1) crackles over Wi-Fi against a host that plays clean to other clients. The Apple client is the one client whose Opus decode lives in core (punktfunk_connection_next_audio_pcm — AudioToolbox has no multistream path), and that decoder only ever decoded packets that ARRIVED. The Linux, Windows and Android decode loops all feed an AudioGapTracker and synthesize libopus packet-loss concealment for every packet the wire lost; the in-core path had the tracker sitting unused in the same crate. So on Apple every lost 5 ms datagram — at ~200 packets/s over Wi-Fi, a steady trickle — landed in the playout ring as a hard time-domain gap: a click per loss, sustained crackle under real loss. The redundant-plane recovery (0xD2) hides single losses when the host grants it, which is exactly why the survivors are the burstier gaps that need concealing most. The decode now runs through the same accounting as everyone else: concealed frames land in front of the arriving frame in one contiguous buffer (the embedder just writes it to its ring), a DTX marker advances the accounting without being decoded, and the output buffer is pre-sized for a full concealment run so the borrow-until-next-call pointer can never dangle. Unit-tested against real libopus: gaps, duplicates, DTX-after-loss, and the 50 ms cap.
This commit is contained in:
@@ -2594,6 +2594,13 @@ PunktfunkStatus punktfunk_connection_end_reason(PunktfunkConnection *c, uint8_t
|
||||
// [`punktfunk_connection_next_audio`] on a given connection, from one dedicated audio thread —
|
||||
// not both (they share the underlying queue).
|
||||
//
|
||||
// **Loss concealment**: packets the wire lost (a gap in the sequence, after the redundant-plane
|
||||
// recovery has had its chance) are synthesized via libopus packet-loss concealment and returned
|
||||
// IN FRONT of the arriving frame in the same buffer — `out->frame_count` then covers the
|
||||
// concealed frames plus the real one (`out->seq`/`out->pts_ns` are the real packet's). The
|
||||
// embedder just writes the whole buffer to its ring, same as any other frame; gaps arrive
|
||||
// pre-healed, exactly as they do on the clients that decode outside core.
|
||||
//
|
||||
// # Safety
|
||||
// `c` is a valid connection handle; `out` is writable. At most one thread pulls audio.
|
||||
PunktfunkStatus punktfunk_connection_next_audio_pcm(PunktfunkConnection *c,
|
||||
|
||||
Reference in New Issue
Block a user