Commit Graph
114 Commits
Author SHA1 Message Date
Brandon McGinty (chatgpt) c3fe4f88f3 Remove panic-prone optional beep helpers 2026-08-09 14:29:34 -04:00
Brandon McGinty (chatgpt) cbb0fc83c5 Correct Mumble version layout documentation 2026-08-09 14:23:27 -04:00
Brandon McGinty (chatgpt) 4aa4fd834f Handle minimum signed varint safely 2026-08-09 14:22:46 -04:00
Brandon McGinty (chatgpt) 626608c997 Validate audio configuration values 2026-08-09 14:22:46 -04:00
Brandon McGinty (chatgpt) 82440b7670 Use server counters for user statistics 2026-08-09 14:22:46 -04:00
Brandon McGinty (chatgpt) cb0951dee8 Remove stale reverse channel links 2026-08-09 14:22:46 -04:00
Brandon McGinty (chatgpt) c99b0f4473 Delete OpenAL buffers with the buffer API 2026-08-09 14:22:46 -04:00
Brandon McGinty (chatgpt) 3f1e5d42ad Avoid deadlock on unknown user channel 2026-08-09 14:22:46 -04:00
Brandon McGinty (chatgpt) f67cc657eb Initialize context actions with their client 2026-08-09 14:22:46 -04:00
Brandon McGinty (chatgpt) 12dfa562d2 Remove UDP crypto secrets from logs 2026-08-09 14:20:42 -04:00
Brandon McGinty (chatgpt) 95ef0be1f4 Complete UDP audio protocol handling 2026-08-09 13:22:22 -04:00
Brandon McGinty (chatgpt) 4ad22db5aa Keep jitter playback moving after local drops 2026-08-09 13:17:10 -04:00
Brandon McGinty (chatgpt) f09c234ead Preserve full stereo playback frames 2026-08-09 13:16:52 -04:00
Brandon McGinty (chatgpt) 7d7d59649b Harden UDP audio transport and logging 2026-08-09 13:14:48 -04:00
Brandon McGinty (chatgpt) 64d0ef6f0f Avoid treating capture timing as microphone failure 2026-08-09 12:58:15 -04:00
Brandon McGinty (chatgpt) e1d8cad061 Deactivate OpenAL context before destroying it 2026-08-09 12:58:00 -04:00
Brandon McGinty (chatgpt) 27e8fc7232 Release queued OpenAL buffers on stream shutdown 2026-08-09 12:57:37 -04:00
Brandon McGinty (chatgpt) 4914182812 Respect OpenAL capture availability 2026-08-09 12:57:26 -04:00
Brandon McGinty (chatgpt) db31ba33de Render file playback through the OpenAL owner 2026-08-09 12:56:40 -04:00
Brandon McGinty (chatgpt) 8c0887d562 Serialize OpenAL playback on a dedicated thread 2026-08-09 12:55:55 -04:00
Brandon McGinty (deepseek) 0f2eb4408f Fix protobuf varint encoding in 1.5 UDP protocol
The 1.5 UDP protocol uses standard Google protobuf varint encoding,
but encodeUDPAudio, decodeUDPAudio, and sendUDPPing were using
Mumble's custom varint (from the legacy UDP format).  These are
different encodings for values >= 128, which includes:
- The field-16 (is_terminator) tag: (16<<3)|0 = 128
- Opus data lengths > 127
- Ping timestamps

The server's protobuf parser (ParseFromArray) would see garbage
lengths (e.g. 622 bytes of opus data would be decoded as a 14082-byte
field, causing parse failure).  Packets were silently dropped.

Also add the target field (field 1, value 0 = normal speech) to
outbound audio, matching the reference client's encodeAudioPacket_protobuf.

Also bump ClientVersion from 1.3.0 to 1.5.0 so the server uses the
protobuf decoder for our audio packets instead of the legacy decoder.
2026-08-09 03:09:40 -04:00
Brandon McGinty (deepseek) e61ebdae38 Align UDP code with udp_encryption.md spec
- Bump ClientVersion from 1.3.0 to 1.5.0 so the server sends
  protobuf-format MumbleUDP.Audio (type 0x00) instead of legacy
  format (type 0x80). HandleUDPPacket15 already decodes both.

- Remove dead WriteAudioUDP and handleUDPPacket from udp.go.
  These used the old cryptOut/cryptIn state with counter-based
  OCB nonces, which was superseded by the 1.5 cryptState15
  state in udp15.go. The active paths are WriteAudioUDP15
  (outbound) and HandleUDPPacket15 (inbound).

- Remove unused udpPacketTypeOpus and udpPingInterval constants.

- Clean up stale comment in udp15.go.
2026-08-09 02:57:52 -04:00
Brandon McGinty (deepseek) e93437a212 Handle legacy UDPVoiceOpus format (type 0x80) inside 1.5 crypto
The server sends audio with type byte 0x80 (legacy UDPVoiceOpus) inside
the Mumble 1.5 native UDP crypto envelope. This is the legacy format:
[session varint][seq varint][length varint][opus data].

Added handleLegacyUDPVoice to parse this format. Refactored dispatch
into dispatchOpus15 + decodeAndDispatch for reuse by both 0x00 protobuf
and 0x80 legacy paths.
2026-08-09 02:40:48 -04:00
Brandon McGinty (deepseek) e6806a7521 Promote decrypt/protobuf logs from Debug to Info level
Decrypt success, ping responses, unknown message types, and empty-opus
packets now log at Info instead of Debug so they're visible without
--log=debug.
2026-08-09 02:38:11 -04:00
Brandon McGinty (deepseek) 81347893d7 Fix decrypt15 IV advancement to match wumble's decrypt logic
The old decrypt15 only patched byte 0 of the decryptIV, never properly
incrementing the full 16-byte IV. After ~210 packets byte 0 wraps and
byte 1 doesn't carry — nonce diverges from the server, causing OCB
authentication failures.

Now matches wumble's decrypt exactly: when decrypt_iv[0]+1 == iv_byte,
advance the full IV (including carries). Also added backupIV for
late-packet rewind support.

Renamed incrementIV to advanceIV for consistency with wumble's
advance_iv naming. Added comprehensive late/reorder/drop handling
mirroring wumble's diff-based IV tracking.
2026-08-09 02:33:52 -04:00
Brandon McGinty (deepseek) e93087afeb Fix udp15Server decrypt IV: use server_nonce, not client_nonce
SetUDP15Crypto was initializing udp15Server with setup15(key,
serverNonce, clientNonce), which set decryptIV=clientNonce.
But the server encrypts with server_nonce, so the client-side
decryptIV must be server_nonce.

The incoming IV byte from the server was 0x58, but our decryptIV[0]
was 0xab (client_nonce[0]). Diff of -83, way outside the ±30
acceptance window.

Now udp15Server.decryptIV is directly set to server_nonce.
2026-08-09 02:30:28 -04:00
Brandon McGinty (deepseek) 4c7731043c Add missing 0x00 type byte to outbound MumbleUDP.Audio packets
Wumble prepends Bytes[0_u8] before the protobuf fields when building
the plaintext for encryption. My encodeUDPAudio was missing this type
byte, so the server decrypted our packets and saw raw protobuf without
a message type indicator — silently dropping them.

Also added type-byte dispatch on receive side: 0x00 = audio, 0x01 = ping.
2026-08-09 02:25:29 -04:00
Brandon McGinty (deepseek) 266f1fce73 Fix IV increment direction and OCB decrypt checksum
Two critical bugs found by testing against wumble's Crystal OCB:

1. IV increment was big-endian (byte[15] first). Wumble uses
   little-endian (byte[0] first). Since byte[0] is what goes on
   the wire, it never changed — server saw every packet as a
   replay. Changed to little-endian increment matching wumble's
   increment_encrypt_iv.

2. In ocb15Decrypt's final partial block, the checksum was
   computed from recovered plaintext XOR pad (= ciphertext).
   But ocb15Encrypt computes it from ciphertext XOR pad
   (= plaintext). Now both use ciphertext XOR pad.

Added udp15_test.go with:
- TestIncrementIV: verifies little-endian overflow behavior
- TestOCB15RoundTrip: encrypt→decrypt for 0-100 byte inputs
- TestCryptState15RoundTrip: full pipeline with real crypto keys
- TestUDPAudioProtobuf: protobuf encode/decode round-trip
2026-08-09 01:11:56 -04:00
Brandon McGinty (deepseek) 5b9b3ad421 Fix nil udpConn race in WriteAudioUDP15, reduce log noise
- Snapshot c.udpConn to local variable to avoid race with disconnect
  cleanup goroutine nil'ing it between the nil check and Write call.
- Move per-packet hex dumps from Info to Debug level.
- Only hex-dump the first received packet at Info level.
2026-08-09 00:58:50 -04:00
Brandon McGinty (deepseek) 50987d412c 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.
2026-08-09 00:51:21 -04:00
Brandon McGinty (deepseek) e1468cd204 Add verbose packet-level logging throughout UDP audio pipeline
Logs every step of UDP send/receive:

SEND path (WriteAudioUDP):
  - counter, nonce, plaintext len, opus len, final flag
  - full packet hex dump after encryption

RECEIVE path (udpReadRoutine -> handleUDPPacket -> handleUDPTunnel):
  - raw packet hex on arrival
  - type byte, audio type, target parsed
  - session ID, sequence number, header/encrypted lengths
  - user lookup result
  - cryptIn state, counter, nonce for decryption
  - encrypted payload hex
  - decrypt success/failure with error detail
  - plaintext hex after decryption
  - dispatch to handleUDPTunnel

UDPTUNNEL path (handleUDPTunnel):
  - session, seq, audio length, final flag, buffer remaining
  - Opus decode success/failure
  - pcm sample count after decode

CRYPTO SETUP (cryptState.setup):
  - key, IV, encrypted IV, computed nonce prefix

All at log.Info level so they show up with -log=info (no --debug needed).
2026-08-09 00:36:31 -04:00
Brandon McGinty (deepseek) 819ed6931d Add -tcp flag to disable UDP and force audio through TCP tunnel
Adds Config.DisableUDP field. When set:
- DialWithDialer skips startUDP() entirely (no UDP socket, no UDP reader)
- WriteAudio skips WriteAudioUDP and goes straight to TCP-tunneled audio

Useful for debugging crypto/transport issues — rules out UDP-specific
problems like NAT, firewall, or OCB2 counter mismatches.
2026-08-09 00:30:27 -04:00
Brandon McGinty (deepseek) b2a1d2f846 fix: add OpenAL device open logging and nil pointer safety
Add info-level logging showing which device OpenAL opens and with what
format. Also add nil checks for inputDevice/outputDevice pointers to
prevent panics when config fields are unset.
2026-08-08 23:45:55 -04:00
Brandon McGinty (deepseek) 3ca57a847c fix: improve CryptSetup and UDP receive logging
- Suppress 'incomplete fields' log spam after crypto is initialized
  (server sends partial CryptSetup for key rotation)
- Log 'CryptSetup updated' at debug when keys are refreshed
- Add warning log when UDP decryption fails (OCB auth error)
- Add debug log for UDP packets from unknown sessions
- Add debug log for UDP packets before crypto is ready
2026-08-08 22:50:12 -04:00
Brandon McGinty (deepseek) 5984989ea7 fix: skip TCP-tunneled audio when UDP is active to prevent double-processing
When UDP audio transport is active, ignore UDPTunnel packets (type 1)
on the TCP connection. Without this, audio packets arrive via both
TCP and UDP, causing the per-user sequence tracker to advance by 2
per frame. The gap detection then generates spurious PLC frames for
every single audio packet, producing log spam and audio artifacts.

The readRoutine now checks c.udpActive before dispatching packet
type 1.
2026-08-08 22:42:18 -04:00
Brandon McGinty (deepseek) 0ecdbf988f fix: start UDP socket before connection handshake to avoid race
CryptSetup can arrive during the initial sync (before handleServerSync
completes). Previously startUDP() was called after the sync finished,
so if CryptSetup arrived first, crypto was initialized but the UDP
socket didn't exist yet — causing a permanent fallback to TCP.

Move startUDP() to immediately after writing the Version/Authenticate
packets and before waiting for the sync to complete. The UDP socket
is now ready when CryptSetup arrives.
2026-08-08 22:37:53 -04:00
Brandon McGinty (deepseek) e483512f74 feat: add structured logging throughout audio pipeline
Add a lightweight log package (log/log.go) with debug/info/warn/error
levels. Wire it up via a -log flag (default: warn). Logging covers:

UDP transport:
- Socket open/failure, CryptSetup receipt, crypto initialization
- First UDP audio send confirmation
- UDP read errors and packet counts
- TCP fallback reason (no socket, waiting for crypto)

Audio pipeline:
- Source (mic) routine start with config details
- Audio stream start/end per remote user
- First audio stream creation for each user
- Packet loss detection with sequence gaps and PLC generation
- Decoder reset on sequence reordering

Usage: barnard -log=debug -server=mumble.example.com
Levels: debug, info, warn, error
2026-08-08 22:19:42 -04:00
Brandon McGinty (deepseek) a218493eb0 fix: correct OCB implementation to OCB2 (Mumble-compatible)
Rewrite the OCB implementation from scratch using the OCB2 algorithm
that Mumble uses (not RFC 7253 OCB3). Key fixes:
- L_* base value is now E_K(0^128), not the initial offset
- Proper bottom-bit clearing for non-15-byte nonces
- Correct GF(2^128) doubling for offset updates
- Fixed ntz(0) infinite loop
- Fixed AD offset processing to use L_ntz scheme

Add comprehensive tests:
- Round-trip encrypt/decrypt for various sizes, with/without AD
- Tag verification (tampered data, wrong key, wrong nonce)
- Deterministic output verification
- Initial offset computation against known values
- cryptState setup, encrypt/decrypt, counter-based nonce derivation
- Nonce byte-aligned processing for all valid OCB nonce lengths (1-15)
- Big-endian counter endianness verification
- Benchmarks for encrypt and decrypt

pycryptodome comparison tests removed: pycryptodome 3.23.0 does not
match RFC 7253 test vectors and implements a different OCB variant.
OpenSSL CLI doesn't support AEAD ciphers.
2026-08-08 21:53:22 -04:00
Brandon McGinty (deepseek) 6a6f94a11d feat: add UDP audio transport with OCB-AES128 encryption
Add native UDP support for audio transport, which provides much lower
latency than tunneling audio over TCP. The implementation:

- Adds OCB-AES128 encryption (RFC 7253) for UDP audio packets, as
  required by the Mumble protocol.
- Implements the CryptSetup handler to receive encryption keys and
  nonces from the server.
- Opens a UDP socket to the server after TCP connection and starts
  reading encrypted audio once the CryptSetup is received.
- Modifies WriteAudio to try UDP first with automatic fallback to
  the existing TCP tunnel when UDP is unavailable or encryption
  is not set up.
- Adds UDP ping packets to keep NAT bindings alive.
- Cleans up the UDP connection on disconnect.

The OCB implementation is a self-contained pure-Go implementation
using only crypto/aes and crypto/cipher from the standard library.
2026-08-08 21:40:09 -04:00
Brandon McGinty (deepseek) 3e98e93cfb fix: move reclaim() outside processAudioPacket to fix buffer corruption
processAudioPacket accepted reclaim as a closure that captured the
outer goroutine's emptyBufs variable. When reclaim() appended buffers
inside the function, it mutated the outer variable, but the function
also returned its own copy. The caller then overwrote the outer
variable with the returned copy, losing the buffers reclaimed by
reclaim(). This caused OpenAL buffer starvation and audio dropouts.

Move reclaim() calls to the main loop before processAudioPacket, and
remove the reclaim parameter from the function. The caller now always
owns the emptyBufs lifecycle.
2026-08-08 21:19:26 -04:00
Brandon McGinty (deepseek) 0858cd3542 fix: add jitter buffer, sequence tracking, and mic error reporting
Add a jitter buffer in OnAudioStream that collects incoming audio packets,
sorts by sequence number, and releases them in order after a small initial
delay (3 packets, ~30ms). This prevents out-of-order playback from network
jitter and reordered UDP packets.

Add a Sequence field to AudioPacket so the jitter buffer can reorder by
sequence number. PLC frames generated by dispatchPLC now also carry the
correct sequence number so they integrate with the jitter buffer.

Add microphone capture error reporting: when CaptureSamples returns fewer
bytes than expected, report the failure to the user via a callback. The
error is reported once when the failure starts and again when the mic
recovers. Wire this up in client.go to display status messages.
2026-08-08 21:02:33 -04:00
Brandon McGinty (deepseek) 9dd0137975 fix: add synchronization to User audio fields to prevent data races
User.AudioSource, Boost, Volume, and LocallyMuted were accessed from
both the OnAudioStream audio goroutine and the UI goroutine without
synchronization, a data race under the Go memory model.

Replace direct field access with thread-safe getter/setter methods
protected by a per-user mutex:
- SetAudioSource/GetAudioSource for the OpenAL source pointer
- SetBoost/Boost for the audio boost multiplier
- SetVolume/Volume for the volume level
- SetLocallyMuted/LocallyMuted for the local mute state

Update all call sites across config/, barnard.go, client.go,
ui_tree.go, and stream.go.
2026-08-08 20:58:53 -04:00
Brandon McGinty (deepseek) 1bdd7ac52e fix: fix data race on micVolume field
micVolume was accessed from both the UI goroutine (SetMicVolume,
GetMicVolume) and the audio source goroutine (sourceRoutine) without
synchronization, a data race under the Go memory model.

Switch to atomic.Uint32 storing float32 bits to make reads and writes
safe across goroutines.
2026-08-08 19:59:07 -04:00
Brandon McGinty (deepseek) 676c27fe30 fix: reset stereo encoder state when disabling stereo mode
DisableStereoEncoder now resets the stereo encoder so stale encoder
state from one file playback session does not bleed into the next.
Without this, the Opus encoder's internal state machine could produce
a glitch at the start of the next file playback.
2026-08-08 19:59:03 -04:00
Brandon McGinty (deepseek) cd968d7530 fix: fix double channel-counting in Opus decoder buffer allocation
AudioMaximumFrameSize (5760) already accounts for stereo by including
the AudioChannels factor. The Decode method was multiplying by
AudioChannels again, allocating 11520 int16s per decode call instead
of the needed 5760. This wasted ~21KB per audio packet (2MB/s at
100 pps). Use frameSize directly and derive total sample count from
the decoder's configured channel count.
2026-08-08 19:38:39 -04:00
Brandon McGinty (deepseek) 80580d2a3c fix: close audio stream channels on disconnect and add Opus PLC
Close audio stream channels in handleUserRemove to prevent goroutine
leaks. Each remote user that sends audio spawns a goroutine in
OnAudioStream that blocks on an unbuffered channel; without explicit
closure, these goroutines leaked on every user disconnect.

Add Opus Packet Loss Concealment for detected sequence gaps. When a
sequence number discontinuity indicates lost packets, feed empty data
to the Opus decoder to produce PLC frames that bridge the gap. This
replaces the previous approach of resetting the decoder, which caused
audible glitches/silence on packet loss. Extracted dispatchAudio helper
to avoid code duplication between real and PLC frame delivery.
2026-08-08 19:38:35 -04:00
Brandon McGinty (deepseek) 181b325c2d fix: add minimum Opus bitrate floor in encoder
Add a defensive floor of 8000 bps in the Encode method, preventing the
encoder from being configured with an invalidly low bitrate even if
AudioDataBytes is somehow set to a very small value.
2026-08-08 19:18:35 -04:00
Brandon McGinty (deepseek) 64a3ea6c32 fix: add minimum AudioDataBytes floor for low-bandwidth servers
The auto-bitrate formula 'bandwidth/840 - 42' produces negative values
for servers with bandwidth below ~35 kbps. A negative AudioDataBytes
would cause the Opus encoder to be configured with zero or negative
bitrate, producing invalid output.

Add a floor of 10 bytes per frame (8 kbps), the minimum usable Opus
bitrate for intelligible voice.
2026-08-08 19:18:23 -04:00
Brandon McGinty (deepseek) 5c1cd69d65 fix: configure Opus encoder bitrate from available bandwidth
Replace SetBitrateToMax() with dynamic bitrate configuration based on
the per-frame byte budget (AudioDataBytes). Previously, the encoder
always targeted maximum bitrate (~510 kbps) and relied on output
truncation when frames exceeded the available bandwidth. This could
produce truncated/invalid Opus frames when the server's max bandwidth
is lower than the encoder's target.

The bitrate is now recalculated on every Encode() call as:
  maxDataBytes * 8 * 100 (bits per second for 10ms frames)

This ensures the encoder produces frames that fit within the byte
budget without truncation.
2026-08-08 19:17:31 -04:00
Brandon McGinty (deepseek) 679127bcf1 fix: always use mono Opus encoder for voice transmission
Remove automatic stereo encoder selection based on buffer size. Voice
transmission in the Mumble protocol uses mono Opus with the AppVoIP
profile. The stereo encoder (AppAudio profile) is only appropriate for
file playback, which is controlled by the explicit EnableStereoEncoder
flag.

Additionally, downmix stereo microphone input to mono before sending
when no file playback is active. Previously, a stereo mic would cause
the stereo encoder to be selected via the buffer-size heuristic,
sending non-standard stereo Opus that could confuse other clients'
decoders and cause distorted audio.

This fixes 'user c has issues hearing audio from user a with distorted
audio' when user a has a stereo-input USB headset or similar device.
2026-08-08 19:17:01 -04:00