diff --git a/gumble/gumble/handlers.go b/gumble/gumble/handlers.go index be02013..e9ab28f 100644 --- a/gumble/gumble/handlers.go +++ b/gumble/gumble/handlers.go @@ -103,7 +103,6 @@ func (c *Client) handleUDPTunnel(buffer []byte) error { decoder := user.decoder if decoder == nil { // TODO: decoder pool - // TODO: de-reference after stream is done codec := c.audioCodec if codec == nil { return errNoCodec @@ -119,18 +118,19 @@ func (c *Client) handleUDPTunnel(buffer []byte) error { } buffer = buffer[n:] - // Detect sequence gaps (packet loss) and reset the decoder to prevent - // permanent audio corruption from state desync. + // Detect sequence gaps (packet loss). Use Opus Packet Loss + // Concealment to fill gaps rather than resetting the decoder, + // which would cause audible glitches. // 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() + // Lost packets detected; generate PLC frames for each. + for i := int64(1); i < gap; i++ { + c.dispatchPLC(user, audioTarget, decoder) + } } else if gap < 0 && gap > -100 { - // Reordered packet — reset to be safe, since the decoder - // state depends on correct frame ordering. + // Reordered packet — reset decoder to prevent corruption. decoder.Reset() } } @@ -175,6 +175,33 @@ func (c *Client) handleUDPTunnel(buffer []byte) error { event.HasPosition = true } + c.dispatchAudio(user, &event) + return nil +} + +// dispatchPLC generates a Packet Loss Concealment frame from the decoder +// and dispatches it to all audio listeners for the given user. +func (c *Client) dispatchPLC(user *User, audioTarget byte, decoder AudioDecoder) { + // Feed empty data to the decoder to trigger Opus PLC, which + // produces a concealed frame bridging the gap. + pcm, err := decoder.Decode(nil, AudioMaximumFrameSize) + if err != nil { + // If PLC fails, reset the decoder so the next real packet + // starts from a clean state. + decoder.Reset() + return + } + event := AudioPacket{ + Client: c, + Sender: user, + Target: &VoiceTarget{ID: uint32(audioTarget)}, + AudioBuffer: AudioBuffer(pcm), + } + c.dispatchAudio(user, &event) +} + +// dispatchAudio sends an audio packet to all registered audio listeners. +func (c *Client) dispatchAudio(user *User, packet *AudioPacket) { c.volatile.Lock() for item := c.Config.AudioListeners.head; item != nil; item = item.next { c.volatile.Unlock() @@ -182,19 +209,17 @@ func (c *Client) handleUDPTunnel(buffer []byte) error { if ch == nil { ch = make(chan *AudioPacket) item.streams[user] = ch - event := AudioStreamEvent{ + streamEvent := AudioStreamEvent{ Client: c, User: user, C: ch, } - item.listener.OnAudioStream(&event) + item.listener.OnAudioStream(&streamEvent) } - ch <- &event + ch <- packet c.volatile.Lock() } c.volatile.Unlock() - - return nil } func (c *Client) handleAuthenticate(buffer []byte) error { @@ -479,6 +504,18 @@ func (c *Client) handleUserRemove(buffer []byte) error { delete(event.User.Channel.Users, session) } delete(c.Users, session) + + // Close audio stream channels for the disconnected user. + // This is safe because handleUserRemove and handleUDPTunnel + // both run in the serialized readRoutine; no concurrent send + // on these channels is possible once the user is removed. + for item := c.Config.AudioListeners.head; item != nil; item = item.next { + if ch, ok := item.streams[event.User]; ok { + close(ch) + delete(item.streams, event.User) + } + } + if packet.Reason != nil { event.String = *packet.Reason }