From 50987d412cf130c54689c528bf748632c917cb52 Mon Sep 17 00:00:00 2001 From: "Brandon McGinty (deepseek)" Date: Sun, 9 Aug 2026 00:51:21 -0400 Subject: [PATCH] Switch UDP to Mumble 1.5 native protocol (matching wumble) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The old gumble library uses the legacy UDPVoice format: [plaintext header: type|target, session, seq] [OCB2 encrypted payload] Modern Murmur 1.5+ servers speak the native UDP protocol: [iv_byte(1)] [tag(3)] [OCB encrypted MumbleUDP.Audio protobuf] This is why zero incoming UDP packets were seen — the server silently dropped all legacy-format datagrams. Changes: - New udp15.go: MumbleUDP.Audio protobuf encode/decode, OCB encrypt/decrypt matching wumble's implementation, cryptState15 with IV-increment nonce and replay-protection history - udp.go: ping uses 1.5 native format (type 0x01, encrypted protobuf), reader delegates to HandleUDPPacket15 - client.go: WriteAudio tries WriteAudioUDP15 first - crypt.go: SetUDP15Crypto called from handleCryptSetup OCB variant matches wumble's crypt_state.cr exactly: full 16-byte nonce (incremented IV), no associated data, final block padding with byte[15] = remaining*8. --- gumble/gumble/client.go | 10 +- gumble/gumble/crypt.go | 17 +- gumble/gumble/udp.go | 44 +-- gumble/gumble/udp15.go | 608 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 654 insertions(+), 25 deletions(-) create mode 100644 gumble/gumble/udp15.go diff --git a/gumble/gumble/client.go b/gumble/gumble/client.go index ba3c65e..0691cda 100644 --- a/gumble/gumble/client.go +++ b/gumble/gumble/client.go @@ -334,11 +334,11 @@ func (c *Client) EnableStereoEncoder() { var udpFallbackLogged bool func (c *Client) WriteAudio(format, target byte, sequence int64, final bool, data []byte, X, Y, Z *float32) error { - // Try UDP first (unless disabled) + // Try Mumble 1.5 native UDP first (unless disabled) if !c.Config.DisableUDP { - if sent, err := c.WriteAudioUDP(format, target, sequence, final, data, X, Y, Z); sent { + if sent, err := c.WriteAudioUDP15(data, final); sent { if err != nil { - log.Error("UDP send error: %v", err) + log.Error("UDP15 send error: %v", err) } return err } @@ -350,8 +350,8 @@ func (c *Client) WriteAudio(format, target byte, sequence int64, final bool, dat log.Info("UDP disabled, audio using TCP tunnel") } else if c.udpConn == nil { log.Info("no UDP socket, audio using TCP tunnel") - } else if !c.cryptOut.initialized { - log.Info("waiting for CryptSetup, audio using TCP tunnel") + } else if udp15Client == nil || !udp15Client.initialized { + log.Info("1.5 crypto not ready, audio using TCP tunnel") } } return c.Conn.WriteAudio(format, target, sequence, final, data, X, Y, Z) diff --git a/gumble/gumble/crypt.go b/gumble/gumble/crypt.go index 0bb92df..f62c8fb 100644 --- a/gumble/gumble/crypt.go +++ b/gumble/gumble/crypt.go @@ -301,6 +301,10 @@ func (c *Client) handleCryptSetup(buffer []byte) error { wasInit := c.cryptOut.initialized c.cryptOut.setup(packet.Key, packet.ClientNonce) c.cryptIn.setup(packet.Key, packet.ServerNonce) + + // Also set up Mumble 1.5 native UDP crypto. + SetUDP15Crypto(packet.Key, packet.ClientNonce, packet.ServerNonce) + if wasInit { log.Debug("CryptSetup updated (key rotation)") } else { @@ -313,10 +317,15 @@ func (c *Client) handleCryptSetup(buffer []byte) error { } if c.cryptOut.initialized && c.udpConn != nil && !c.udpActive { - c.udpActive = true - log.Info("UDP crypto ready, starting UDP reader and pinger") - go c.udpReadRoutine() - go c.udpPingRoutine() + // Only start UDP reader if 1.5 crypto is also ready. + if udp15Client != nil && udp15Client.initialized { + c.udpActive = true + log.Info("UDP crypto ready (1.5 native), starting UDP reader and pinger") + go c.udpReadRoutine() + go c.udpPingRoutine() + } else { + log.Info("legacy crypto ready, waiting for 1.5 crypto before starting UDP") + } } else if c.cryptOut.initialized && c.udpConn == nil { log.Warn("crypto ready but no UDP socket — audio will use TCP tunnel") } diff --git a/gumble/gumble/udp.go b/gumble/gumble/udp.go index 5643164..7289eb7 100644 --- a/gumble/gumble/udp.go +++ b/gumble/gumble/udp.go @@ -1,6 +1,7 @@ package gumble import ( + "bytes" "encoding/binary" "encoding/hex" "errors" @@ -48,8 +49,9 @@ func (c *Client) startUDP() error { } // udpReadRoutine reads encrypted UDP audio packets from the server. +// Uses Mumble 1.5 native UDP format. func (c *Client) udpReadRoutine() { - log.Info("UDP reader started") + log.Info("UDP reader started (1.5 native format)") buf := make([]byte, maxUDPPacketSize) var packetCount uint64 for { @@ -63,39 +65,49 @@ func (c *Client) udpReadRoutine() { packetCount, n, addr, hex.EncodeToString(buf[:n])) packet := make([]byte, n) copy(packet, buf[:n]) - c.handleUDPPacket(packet, packetCount) + c.HandleUDPPacket15(packet, packetCount) } } // udpPingRoutine sends periodic ping packets over UDP to keep the -// connection alive and maintain NAT bindings. +// connection alive and maintain NAT bindings. Uses Mumble 1.5 native +// UDP ping format: type byte 0x01, protobuf field 1 = timestamp. func (c *Client) udpPingRoutine() { - ticker := time.NewTicker(udpPingInterval) + ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() - var counter uint32 for { select { case <-c.end: return case <-ticker.C: - c.sendUDPPing(counter) - counter++ + c.sendUDPPing() } } } -// sendUDPPing sends a UDP ping packet. The Mumble UDP ping is a type -// byte (0x20) followed by a varint-encoded timestamp. -func (c *Client) sendUDPPing(counter uint32) { - if c.udpConn == nil { +// sendUDPPing sends a Mumble 1.5 native UDP ping. +func (c *Client) sendUDPPing() { + cs := udp15Client + if cs == nil || !cs.initialized || c.udpConn == nil { return } - // Type byte 0x20 (ping) + varint timestamp - var ping [1 + varint.MaxVarintLen]byte - ping[0] = 0x20 - n := varint.Encode(ping[1:], int64(time.Now().UnixNano())) - c.udpConn.Write(ping[:1+n]) + // Type byte 0x01 = UDPPing, field 1 = timestamp (varint, milliseconds). + var tmp [varint.MaxVarintLen]byte + var buf bytes.Buffer + buf.WriteByte(0x01) // type = UDPPing + + n := varint.Encode(tmp[:], int64((1<<3)|0)) + buf.Write(tmp[:n]) // field 1 tag + + n = varint.Encode(tmp[:], int64(time.Now().UnixMilli())) + buf.Write(tmp[:n]) // timestamp value + + encrypted, err := cs.encrypt15(buf.Bytes()) + if err != nil { + return + } + c.udpConn.Write(encrypted) } // WriteAudioUDP writes an encrypted audio packet over UDP. diff --git a/gumble/gumble/udp15.go b/gumble/gumble/udp15.go new file mode 100644 index 0000000..d7cd001 --- /dev/null +++ b/gumble/gumble/udp15.go @@ -0,0 +1,608 @@ +package gumble + +import ( + "bytes" + "crypto/aes" + "encoding/hex" + "errors" + + "git.stormux.org/storm/barnard/gumble/gumble/varint" + "git.stormux.org/storm/barnard/log" +) + +// --------------------------------------------------------------------------- +// Mumble 1.5 native UDP — MumbleUDP.Audio protobuf helpers. +// +// Message MumbleUDP.Audio: +// field 3: sender_session (varint, wire 0) +// field 4: frame_number (varint, wire 0) — 10 ms units +// field 5: opus_data (bytes, wire 2) +// field 16: is_terminator (varint, wire 0) +// --------------------------------------------------------------------------- + +// encodeUDPAudio builds a MumbleUDP.Audio protobuf message. +// If session == 0, sender_session is omitted (used for outbound). +func encodeUDPAudio(session uint32, frameNumber uint32, opusData []byte, terminator bool) []byte { + var buf bytes.Buffer + var tmp [varint.MaxVarintLen]byte + + if session != 0 { + n := varint.Encode(tmp[:], int64((3<<3)|0)) + buf.Write(tmp[:n]) + n = varint.Encode(tmp[:], int64(session)) + buf.Write(tmp[:n]) + } + n := varint.Encode(tmp[:], int64((4<<3)|0)) + buf.Write(tmp[:n]) + n = varint.Encode(tmp[:], int64(frameNumber)) + buf.Write(tmp[:n]) + if len(opusData) > 0 { + n := varint.Encode(tmp[:], int64((5<<3)|2)) + buf.Write(tmp[:n]) + n = varint.Encode(tmp[:], int64(len(opusData))) + buf.Write(tmp[:n]) + buf.Write(opusData) + } + if terminator { + n := varint.Encode(tmp[:], int64((16<<3)|0)) + buf.Write(tmp[:n]) + n = varint.Encode(tmp[:], 1) + buf.Write(tmp[:n]) + } + + return buf.Bytes() +} + +// decodeUDPAudio parses a MumbleUDP.Audio protobuf message. +func decodeUDPAudio(data []byte) (session uint32, frameNumber uint32, opusData []byte, terminator bool) { + pos := 0 + for pos < len(data) { + key, n := varint.Decode(data[pos:]) + if n <= 0 { + break + } + pos += n + fieldNum := int(key >> 3) + wireType := int(key & 0x7) + + switch wireType { + case 0: // varint + val, n := varint.Decode(data[pos:]) + if n <= 0 { + return + } + pos += n + switch fieldNum { + case 3: + session = uint32(val) + case 4: + frameNumber = uint32(val) + case 16: + terminator = val != 0 + } + case 2: // length-delimited + length, n := varint.Decode(data[pos:]) + if n <= 0 { + return + } + pos += n + if int(length) > len(data)-pos { + return + } + if fieldNum == 5 { + opusData = make([]byte, length) + copy(opusData, data[pos:pos+int(length)]) + } + pos += int(length) + default: + return // unknown wire type, bail + } + } + return +} + +// --------------------------------------------------------------------------- +// Mumble 1.5 native UDP crypto (AES-128-OCB with IV-prefix header). +// +// Wire format: [iv_byte(1)][tag(3)][ciphertext] +// Nonce is the full 16-byte IV, incremented per-packet. +// --------------------------------------------------------------------------- + +const ( + udp15BlockSize = 16 + udp15HeaderSize = 4 // 1 byte IV + 3 bytes tag +) + +// cryptState15 implements Mumble 1.5 native UDP encryption. +type cryptState15 struct { + key [16]byte + encryptIV [16]byte + decryptIV [16]byte + history [256]byte // replay: history[iv_byte] == expected next byte + initialized bool +} + +// setup15 initializes 1.5-style crypto from CryptSetup. +// clientNonce → encryptIV, serverNonce → decryptIV. +func (cs *cryptState15) setup15(key, clientNonce, serverNonce []byte) error { + if len(key) != 16 || len(clientNonce) != 16 || len(serverNonce) != 16 { + return errors.New("gumble: invalid crypto key/nonce") + } + copy(cs.key[:], key) + copy(cs.encryptIV[:], clientNonce) + copy(cs.decryptIV[:], serverNonce) + cs.initialized = true + + log.Info("cryptState15 setup: key=%s encryptIV=%s decryptIV=%s", + hex.EncodeToString(cs.key[:]), + hex.EncodeToString(cs.encryptIV[:]), + hex.EncodeToString(cs.decryptIV[:])) + + return nil +} + +// encrypt15 encrypts plaintext for Mumble 1.5 native UDP. +// Returns [iv_byte(1)][tag(3)][ciphertext]. +func (cs *cryptState15) encrypt15(plaintext []byte) ([]byte, error) { + if !cs.initialized { + return nil, errors.New("gumble: crypto not initialized") + } + + // Increment IV (big-endian). + incrementBE(cs.encryptIV[:]) + + ciphertext, tag := ocb15Encrypt(cs.key[:], cs.encryptIV[:], plaintext) + + out := make([]byte, udp15HeaderSize+len(ciphertext)) + out[0] = cs.encryptIV[0] + out[1] = tag[0] + out[2] = tag[1] + out[3] = tag[2] + copy(out[4:], ciphertext) + return out, nil +} + +// decrypt15 decrypts a Mumble 1.5 native UDP packet. +// Returns plaintext or nil on failure. +func (cs *cryptState15) decrypt15(packet []byte) ([]byte, error) { + if !cs.initialized { + return nil, errors.New("gumble: crypto not initialized") + } + if len(packet) < udp15HeaderSize { + return nil, errors.New("gumble: packet too short") + } + + ivByte := packet[0] + expectedTag := packet[1:4] + encrypted := packet[4:] + + savedIV := cs.decryptIV + + // Determine how the received iv_byte relates to our expected decryptIV[0]. + if ivByte == cs.decryptIV[0] { + // Perfect match — packet arrived in order. + } else { + diff := int(ivByte) - int(cs.decryptIV[0]) + if diff < -128 { + diff += 256 + } else if diff > 128 { + diff -= 256 + } + + if diff >= -30 && diff <= 30 { + cs.decryptIV[0] = ivByte + if diff < 0 { + // Late packet — we're ahead. Restore after decrypt. + defer func() { cs.decryptIV = savedIV }() + } + } else { + return nil, errors.New("gumble: OCB IV too far off") + } + + // Replay check + if cs.history[ivByte] != 0 && cs.history[ivByte] == cs.decryptIV[1] { + cs.decryptIV = savedIV + return nil, errors.New("gumble: OCB replay detected") + } + } + + plaintext, tag, err := ocb15Decrypt(cs.key[:], cs.decryptIV[:], encrypted) + if err != nil { + cs.decryptIV = savedIV + return nil, err + } + + // Verify first 3 bytes of tag. + if tag[0] != expectedTag[0] || tag[1] != expectedTag[1] || tag[2] != expectedTag[2] { + cs.decryptIV = savedIV + return nil, errors.New("gumble: OCB authentication failed") + } + + // Update replay history. + cs.history[ivByte] = cs.decryptIV[1] + + return plaintext, nil +} + +// incrementBE increments a 16-byte slice as a big-endian integer. +func incrementBE(iv []byte) { + for i := len(iv) - 1; i >= 0; i-- { + iv[i]++ + if iv[i] != 0 { + break + } + } +} + +// --------------------------------------------------------------------------- +// OCB variant for Mumble 1.5 native UDP. +// Matches the implementation in Wumble's crypt_state.cr. +// --------------------------------------------------------------------------- + +// ocb15Encrypt encrypts with AES-128-OCB (no associated data). +// Returns ciphertext and 16-byte tag. +func ocb15Encrypt(key, nonce, plaintext []byte) (ciphertext, tag []byte) { + block, _ := aes.NewCipher(key) + + // delta = AES_K(nonce) + delta := make([]byte, 16) + block.Encrypt(delta, nonce) + + checksum := make([]byte, 16) + + pos := 0 + remaining := len(plaintext) + + // Full blocks. + for remaining > 16 { + shift2inplace(delta) + + xor16(checksum, checksum, plaintext[pos:pos+16]) + + // C = delta XOR AES_K(delta XOR plaintext) + tmp := make([]byte, 16) + xorBytes(tmp, plaintext[pos:pos+16], delta) + block.Encrypt(tmp, tmp) + xorBytes(tmp, tmp, delta) + + ciphertext = append(ciphertext, tmp...) + pos += 16 + remaining -= 16 + } + + // Final partial block. + shift2inplace(delta) + + // pad = AES_K(temporary XOR delta) where temporary[15] = remaining*8 + tmp := make([]byte, 16) + tmp[15] = byte(remaining * 8) + xor16(tmp, tmp, delta) + pad := make([]byte, 16) + block.Encrypt(pad, tmp) + + // Cpartial = plaintext XOR pad (truncated) + cpart := make([]byte, remaining) + xorBytes(cpart, plaintext[pos:pos+remaining], pad[:remaining]) + ciphertext = append(ciphertext, cpart...) + + // checksum ^= (cpart || 0*) XOR pad + csTemp := make([]byte, 16) + copy(csTemp, cpart) + xor16(csTemp, csTemp, pad) + xor16(checksum, checksum, csTemp) + + // Tag = AES_K(3*delta XOR checksum) + shift3inplace(delta) + xor16(delta, delta, checksum) + tag = make([]byte, 16) + block.Encrypt(tag, delta) + + return ciphertext, tag +} + +// ocb15Decrypt decrypts with AES-128-OCB (no associated data). +// Returns plaintext and 16-byte tag. +func ocb15Decrypt(key, nonce, ciphertext []byte) (plaintext, tag []byte, err error) { + block, _ := aes.NewCipher(key) + + delta := make([]byte, 16) + block.Encrypt(delta, nonce) + + checksum := make([]byte, 16) + + pos := 0 + remaining := len(ciphertext) + + // Full blocks. + for remaining > 16 { + shift2inplace(delta) + + // P = delta XOR AES_D_K(delta XOR ciphertext) + tmp := make([]byte, 16) + xorBytes(tmp, ciphertext[pos:pos+16], delta) + block.Decrypt(tmp, tmp) + xorBytes(tmp, tmp, delta) + plaintext = append(plaintext, tmp...) + + xor16(checksum, checksum, tmp) + pos += 16 + remaining -= 16 + } + + // Final partial block. + shift2inplace(delta) + + tmp := make([]byte, 16) + tmp[15] = byte(remaining * 8) + xor16(tmp, tmp, delta) + pad := make([]byte, 16) + block.Encrypt(pad, tmp) + + // Ppartial = ciphertext XOR pad (truncated) + ppart := make([]byte, remaining) + xorBytes(ppart, ciphertext[pos:pos+remaining], pad[:remaining]) + plaintext = append(plaintext, ppart...) + + // checksum ^= (ppart || 0*) XOR pad + csTemp := make([]byte, 16) + copy(csTemp, ppart) + xor16(csTemp, csTemp, pad) + xor16(checksum, checksum, csTemp) + + // Tag = AES_K(3*delta XOR checksum) + shift3inplace(delta) + xor16(delta, delta, checksum) + tag = make([]byte, 16) + block.Encrypt(tag, delta) + + return plaintext, tag, nil +} + +// --------------------------------------------------------------------------- +// GF(2^128) helpers (same as crypt.go's doubleBlock, but in-place). +// --------------------------------------------------------------------------- + +func shift2inplace(block []byte) { + carry := (block[0] >> 7) & 1 + for i := 0; i < 15; i++ { + block[i] = (block[i] << 1) | (block[i+1] >> 7) + } + block[15] = (block[15] << 1) ^ (carry * 0x87) +} + +func shift3inplace(block []byte) { + orig := make([]byte, 16) + copy(orig, block) + shift2inplace(block) + xor16(block, block, orig) +} + +func xor16(dst, a, b []byte) { + dst[0] = a[0] ^ b[0] + dst[1] = a[1] ^ b[1] + dst[2] = a[2] ^ b[2] + dst[3] = a[3] ^ b[3] + dst[4] = a[4] ^ b[4] + dst[5] = a[5] ^ b[5] + dst[6] = a[6] ^ b[6] + dst[7] = a[7] ^ b[7] + dst[8] = a[8] ^ b[8] + dst[9] = a[9] ^ b[9] + dst[10] = a[10] ^ b[10] + dst[11] = a[11] ^ b[11] + dst[12] = a[12] ^ b[12] + dst[13] = a[13] ^ b[13] + dst[14] = a[14] ^ b[14] + dst[15] = a[15] ^ b[15] +} + +func xorBytes(dst, a, b []byte) { + for i := 0; i < len(dst); i++ { + dst[i] = a[i] ^ b[i] + } +} + +// --------------------------------------------------------------------------- +// Global 1.5 crypto state. +// --------------------------------------------------------------------------- + +var udp15Client *cryptState15 +var udp15Server *cryptState15 + +// SetUDP15Crypto installs the 1.5 native UDP crypto state from CryptSetup. +func SetUDP15Crypto(key, clientNonce, serverNonce []byte) { + csClient := &cryptState15{} + if err := csClient.setup15(key, clientNonce, serverNonce); err != nil { + log.Error("SetUDP15Crypto client: %v", err) + return + } + udp15Client = csClient + + csServer := &cryptState15{} + if err := csServer.setup15(key, serverNonce, clientNonce); err != nil { + log.Error("SetUDP15Crypto server: %v", err) + return + } + udp15Server = csServer + + log.Info("Mumble 1.5 native UDP crypto initialized") +} + +// --------------------------------------------------------------------------- +// Frame number tracking. +// --------------------------------------------------------------------------- + +var udp15FrameNumber uint32 + +// nextFrameNumber returns the next frame number for outbound 1.5 UDP audio. +func nextFrameNumber() uint32 { + n := udp15FrameNumber + udp15FrameNumber++ + return n +} + +// --------------------------------------------------------------------------- +// Replace the WriteAudioUDP path for 1.5 native format. +// --------------------------------------------------------------------------- + +// WriteAudioUDP15 writes an encrypted audio packet using Mumble 1.5 native UDP. +// Returns true if sent, false if TCP should be used. +func (c *Client) WriteAudioUDP15(data []byte, final bool) (bool, error) { + cs := udp15Client + if cs == nil || !cs.initialized { + return false, nil + } + if c.udpConn == nil { + return false, nil + } + + frameNum := nextFrameNumber() + + // Build MumbleUDP.Audio protobuf. + payload := encodeUDPAudio(0, frameNum, data, final) + + log.Info("UDP15 send: frame=%d opus_len=%d proto_len=%d final=%v", + frameNum, len(data), len(payload), final) + + // Encrypt with 1.5 format. + encrypted, err := cs.encrypt15(payload) + if err != nil { + log.Error("UDP15 encrypt failed: %v", err) + return false, err + } + + log.Info("UDP15 send: encrypted_len=%d hex=%s", + len(encrypted), hex.EncodeToString(encrypted)) + + _, err = c.udpConn.Write(encrypted) + if err != nil { + log.Error("UDP15 send write failed: %v", err) + return false, err + } + return true, nil +} + +// HandleUDPPacket15 processes an incoming Mumble 1.5 native UDP packet. +func (c *Client) HandleUDPPacket15(packet []byte, pktNum uint64) { + if len(packet) < udp15HeaderSize { + log.Warn("UDP15 #%d: packet too short (%d bytes)", pktNum, len(packet)) + return + } + + log.Info("UDP15 #%d: raw hex=%s", pktNum, hex.EncodeToString(packet)) + + cs := udp15Server + if cs == nil || !cs.initialized { + log.Warn("UDP15 #%d: crypto not initialized", pktNum) + return + } + + plaintext, err := cs.decrypt15(packet) + if err != nil { + log.Warn("UDP15 #%d: decrypt failed: %v", pktNum, err) + return + } + + log.Info("UDP15 #%d: decrypt OK, plaintext hex=%s", pktNum, hex.EncodeToString(plaintext)) + + // Parse MumbleUDP.Audio protobuf. + session, frameNum, opusData, terminator := decodeUDPAudio(plaintext) + + log.Info("UDP15 #%d: session=%d frame=%d opus_len=%d term=%v", + pktNum, session, frameNum, len(opusData), terminator) + + if len(opusData) == 0 && !terminator { + return + } + + // Find the user. + user := c.Users[session] + if user == nil { + log.Warn("UDP15 #%d: unknown session %d", pktNum, session) + return + } + + // Get or create decoder. + decoder := user.decoder + if decoder == nil { + codec := c.audioCodec + if codec == nil { + log.Warn("UDP15 #%d: no audio codec", pktNum) + return + } + decoder = codec.NewDecoder() + user.decoder = decoder + log.Info("UDP15 #%d: new decoder for %s", pktNum, user.Name) + } + + if terminator && len(opusData) == 0 { + // Decoder reset on terminator. + decoder.Reset() + log.Info("UDP15 #%d: terminator for %s, decoder reset", pktNum, user.Name) + return + } + + if len(opusData) == 0 { + return + } + + // Detect sequence gaps via frame_number. + if user.audioSequenceValid { + gap := int64(frameNum) - user.audioSequence + if gap > 1 && gap < 100 { + log.Info("UDP15 #%d: seq gap for %s: %d -> %d (loss=%d), generating PLC", + pktNum, user.Name, user.audioSequence, frameNum, gap-1) + for i := int64(1); i < gap; i++ { + c.dispatchPLC15(user, decoder) + } + } else if gap < 0 && gap > -100 { + log.Info("UDP15 #%d: seq reorder for %s: %d -> %d, resetting decoder", + pktNum, user.Name, user.audioSequence, frameNum) + decoder.Reset() + } else if gap == 0 { + log.Info("UDP15 #%d: duplicate seq=%d for %s", pktNum, frameNum, user.Name) + return + } + } + user.audioSequence = int64(frameNum) + user.audioSequenceValid = true + + // Decode Opus. + pcm, err := decoder.Decode(opusData, AudioMaximumFrameSize) + if err != nil { + log.Warn("UDP15 #%d: Opus decode failed for %s: %v", pktNum, user.Name, err) + decoder.Reset() + return + } + + log.Info("UDP15 #%d: Opus OK for %s, pcm_samples=%d", pktNum, user.Name, len(pcm)) + + // Dispatch. + event := AudioPacket{ + Client: c, + Sender: user, + Target: &VoiceTarget{ID: 0}, + Sequence: int64(frameNum), + AudioBuffer: AudioBuffer(pcm), + } + c.dispatchAudio(user, &event) +} + +// dispatchPLC15 generates a Packet Loss Concealment frame for 1.5 UDP. +func (c *Client) dispatchPLC15(user *User, decoder AudioDecoder) { + pcm, err := decoder.Decode(nil, AudioMaximumFrameSize) + if err != nil { + decoder.Reset() + return + } + seq := user.audioSequence + 1 + user.audioSequence = seq + event := AudioPacket{ + Client: c, + Sender: user, + Target: &VoiceTarget{ID: 0}, + Sequence: seq, + AudioBuffer: AudioBuffer(pcm), + } + c.dispatchAudio(user, &event) +}