From dfa184211ee7b8f2ca1347b1d1ae076064b992cb Mon Sep 17 00:00:00 2001 From: "Brandon McGinty (deepseek)" Date: Sat, 8 Aug 2026 19:15:53 -0400 Subject: [PATCH] fix: reset Opus decoder on packet loss and decode errors Track per-user audio sequence numbers to detect UDP packet loss gaps. When a sequence discontinuity is detected (loss, reorder, or burst gap), reset the Opus decoder state to prevent permanent audio corruption. Also reset the decoder when Decode() returns an error, instead of leaving the decoder in a corrupted state that produces static/popping for the remainder of the session. This fixes the 'random static/popping from user b but no other clients hear it' symptom, which occurs when one client experiences packet loss affecting only its own per-user decoder instance. --- gumble/gumble/handlers.go | 23 +++++++++++++++++++++-- gumble/gumble/user.go | 5 +++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/gumble/gumble/handlers.go b/gumble/gumble/handlers.go index a0278da..be02013 100644 --- a/gumble/gumble/handlers.go +++ b/gumble/gumble/handlers.go @@ -113,13 +113,30 @@ func (c *Client) handleUDPTunnel(buffer []byte) error { } // Sequence - // TODO: use in jitter buffer - _, n = varint.Decode(buffer) + seq, n := varint.Decode(buffer) if n <= 0 { return errInvalidProtobuf } buffer = buffer[n:] + // Detect sequence gaps (packet loss) and reset the decoder to prevent + // permanent audio corruption from state desync. + // Mumble uses a monotonically increasing sequence that wraps at MaxInt32. + if user.audioSequenceValid { + // Only treat as discontinuity if the gap is small enough to be loss + // rather than a legitimate wrap-around or restart. + gap := seq - user.audioSequence + if gap > 1 && gap < 100 { + decoder.Reset() + } else if gap < 0 && gap > -100 { + // Reordered packet — reset to be safe, since the decoder + // state depends on correct frame ordering. + decoder.Reset() + } + } + user.audioSequence = seq + user.audioSequenceValid = true + // Length length, n := varint.Decode(buffer) if n <= 0 { @@ -134,6 +151,8 @@ func (c *Client) handleUDPTunnel(buffer []byte) error { pcm, err := decoder.Decode(buffer[:audioLength], AudioMaximumFrameSize) if err != nil { + // Decode failure indicates corrupted decoder state; reset and drop. + decoder.Reset() return err } diff --git a/gumble/gumble/user.go b/gumble/gumble/user.go index d23ffe2..ef9546c 100644 --- a/gumble/gumble/user.go +++ b/gumble/gumble/user.go @@ -53,6 +53,11 @@ type User struct { client *Client decoder AudioDecoder + // audioSequence tracks the last UDP audio packet sequence number for + // this user, used to detect packet loss and reset the Opus decoder. + audioSequence int64 + audioSequenceValid bool + AudioSource *openal.Source Boost uint16 Volume float32