Switch UDP to Mumble 1.5 native protocol (matching wumble)

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.
This commit is contained in:
Brandon McGinty (deepseek)
2026-08-09 00:51:21 -04:00
committed by Brandon McGinty
parent e1468cd204
commit 50987d412c
4 changed files with 654 additions and 25 deletions
+5 -5
View File
@@ -334,11 +334,11 @@ func (c *Client) EnableStereoEncoder() {
var udpFallbackLogged bool var udpFallbackLogged bool
func (c *Client) WriteAudio(format, target byte, sequence int64, final bool, data []byte, X, Y, Z *float32) error { 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 !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 { if err != nil {
log.Error("UDP send error: %v", err) log.Error("UDP15 send error: %v", err)
} }
return 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") log.Info("UDP disabled, audio using TCP tunnel")
} else if c.udpConn == nil { } else if c.udpConn == nil {
log.Info("no UDP socket, audio using TCP tunnel") log.Info("no UDP socket, audio using TCP tunnel")
} else if !c.cryptOut.initialized { } else if udp15Client == nil || !udp15Client.initialized {
log.Info("waiting for CryptSetup, audio using TCP tunnel") log.Info("1.5 crypto not ready, audio using TCP tunnel")
} }
} }
return c.Conn.WriteAudio(format, target, sequence, final, data, X, Y, Z) return c.Conn.WriteAudio(format, target, sequence, final, data, X, Y, Z)
+10 -1
View File
@@ -301,6 +301,10 @@ func (c *Client) handleCryptSetup(buffer []byte) error {
wasInit := c.cryptOut.initialized wasInit := c.cryptOut.initialized
c.cryptOut.setup(packet.Key, packet.ClientNonce) c.cryptOut.setup(packet.Key, packet.ClientNonce)
c.cryptIn.setup(packet.Key, packet.ServerNonce) 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 { if wasInit {
log.Debug("CryptSetup updated (key rotation)") log.Debug("CryptSetup updated (key rotation)")
} else { } else {
@@ -313,10 +317,15 @@ func (c *Client) handleCryptSetup(buffer []byte) error {
} }
if c.cryptOut.initialized && c.udpConn != nil && !c.udpActive { if c.cryptOut.initialized && c.udpConn != nil && !c.udpActive {
// Only start UDP reader if 1.5 crypto is also ready.
if udp15Client != nil && udp15Client.initialized {
c.udpActive = true c.udpActive = true
log.Info("UDP crypto ready, starting UDP reader and pinger") log.Info("UDP crypto ready (1.5 native), starting UDP reader and pinger")
go c.udpReadRoutine() go c.udpReadRoutine()
go c.udpPingRoutine() 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 { } else if c.cryptOut.initialized && c.udpConn == nil {
log.Warn("crypto ready but no UDP socket — audio will use TCP tunnel") log.Warn("crypto ready but no UDP socket — audio will use TCP tunnel")
} }
+28 -16
View File
@@ -1,6 +1,7 @@
package gumble package gumble
import ( import (
"bytes"
"encoding/binary" "encoding/binary"
"encoding/hex" "encoding/hex"
"errors" "errors"
@@ -48,8 +49,9 @@ func (c *Client) startUDP() error {
} }
// udpReadRoutine reads encrypted UDP audio packets from the server. // udpReadRoutine reads encrypted UDP audio packets from the server.
// Uses Mumble 1.5 native UDP format.
func (c *Client) udpReadRoutine() { func (c *Client) udpReadRoutine() {
log.Info("UDP reader started") log.Info("UDP reader started (1.5 native format)")
buf := make([]byte, maxUDPPacketSize) buf := make([]byte, maxUDPPacketSize)
var packetCount uint64 var packetCount uint64
for { for {
@@ -63,39 +65,49 @@ func (c *Client) udpReadRoutine() {
packetCount, n, addr, hex.EncodeToString(buf[:n])) packetCount, n, addr, hex.EncodeToString(buf[:n]))
packet := make([]byte, n) packet := make([]byte, n)
copy(packet, buf[:n]) copy(packet, buf[:n])
c.handleUDPPacket(packet, packetCount) c.HandleUDPPacket15(packet, packetCount)
} }
} }
// udpPingRoutine sends periodic ping packets over UDP to keep the // 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() { func (c *Client) udpPingRoutine() {
ticker := time.NewTicker(udpPingInterval) ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop() defer ticker.Stop()
var counter uint32
for { for {
select { select {
case <-c.end: case <-c.end:
return return
case <-ticker.C: case <-ticker.C:
c.sendUDPPing(counter) c.sendUDPPing()
counter++
} }
} }
} }
// sendUDPPing sends a UDP ping packet. The Mumble UDP ping is a type // sendUDPPing sends a Mumble 1.5 native UDP ping.
// byte (0x20) followed by a varint-encoded timestamp. func (c *Client) sendUDPPing() {
func (c *Client) sendUDPPing(counter uint32) { cs := udp15Client
if c.udpConn == nil { if cs == nil || !cs.initialized || c.udpConn == nil {
return return
} }
// Type byte 0x20 (ping) + varint timestamp // Type byte 0x01 = UDPPing, field 1 = timestamp (varint, milliseconds).
var ping [1 + varint.MaxVarintLen]byte var tmp [varint.MaxVarintLen]byte
ping[0] = 0x20 var buf bytes.Buffer
n := varint.Encode(ping[1:], int64(time.Now().UnixNano())) buf.WriteByte(0x01) // type = UDPPing
c.udpConn.Write(ping[:1+n])
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. // WriteAudioUDP writes an encrypted audio packet over UDP.
+608
View File
@@ -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)
}