100 Commits
Author SHA1 Message Date
Brandon McGintyandClaude Opus 5 ecf00271b3 add a memory watcher script
Records resident memory alongside the Go heap size and dumps heap and
goroutine profiles once memory passes a threshold, while the process is
still alive to ask.

The ratio between the two numbers is what identifies the source: barnard's
Go heap sits at a few megabytes in normal operation, so memory growth with a
flat Go heap points at cgo allocations in OpenAL, opus or rnnoise, which the
Go collector cannot see and therefore never applies back pressure to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 12:32:52 -04:00
Brandon McGintyandClaude Opus 5 31ae03ad65 bound the per-source recording backlog
Each tick drains one fixed chunk per source, so if the encoder stalls the
loop never makes the deficit up and the backlog only grows from there. The
queues had no cap, so a long recording against a slow encoder grew for as
long as it ran.

Cap each queue and keep the newest audio; discarding the newest instead
would only push the recording further behind.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 12:32:44 -04:00
Brandon McGintyandClaude Opus 5 53894b33ae do not block the read routine on a late reject
The connect channel was unbuffered. Once DialWithDialer has returned on its
synchronization timeout nothing reads that channel again, so a Reject
arriving afterwards blocked handleReject, and with it readRoutine, forever.
That leaks the goroutine along with the client, its user and channel maps
and its read buffer, and because readRoutine never reaches its exit path the
ping and UDP routines are never signalled to stop either.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 12:32:44 -04:00
Brandon McGintyandClaude Opus 5 fe322ce067 release the read buffer after an oversized packet
The buffer grew to the largest packet ever received and was never given
back, so a single large ACL, user list or channel comment pinned its full
size, up to the ten megabyte packet limit, for the life of the connection.

Keep a modest buffer between packets and allocate larger ones only for as
long as they are needed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 12:32:44 -04:00
Brandon McGintyandClaude Opus 5 ef9bd19fc9 bound the configured audio buffer count
Buffers had a lower bound but no upper one, and it is allocated per speaking
user twice over: as a queue of decoded frames and as OpenAL playback
buffers. Each buffer holds up to one maximum sized frame, so a large value
multiplied by a populated channel is a substantial amount of memory.

Cap it, and reject the flag up front so the failure names the flag rather
than surfacing later as a dial error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 12:32:44 -04:00
Brandon McGintyandClaude Opus 5 0d62f745ca reuse the stereo Opus encoder across connections
Every connect built a fresh stereo encoder for file playback. Each one holds
a little under a megabyte of encoder state, so on a flaky link that is close
to a megabyte of churn per reconnect for no benefit; the encoder is already
reset when file playback ends.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 12:32:30 -04:00
Brandon McGintyandClaude Opus 5 5cdb2684b5 keep the tone test output open across a reconnect
The saver reserves its output file exclusively, so opening it a second time
fails. connect opened a new one per connection and a disconnect closed it,
which meant the first reconnect died with "file exists" instead of resuming.

Open the saver once and re-attach it on reconnect, and close it on exit
instead. Detaching and closing are now separate, since only shutdown wants
both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 12:32:21 -04:00
Brandon McGintyandClaude Opus 5 77fad24560 release the audio stream a reconnect replaces
connect assigned b.Stream without releasing the stream already there, and
OnDisconnect started a reconnect loop unconditionally, so two connects could
race to install a stream. The loser was simply overwritten.

An overwritten stream is never destroyed, so it keeps its OpenAL device, its
render thread, and its entry in the shared audio listener list, which only
Destroy removes. It therefore stays subscribed for the life of the process
and every later audio packet from every user is dispatched to it as well: a
goroutine, a packet queue and a set of playback buffers per orphan, per
user. The file playback stream and the tone test saver were replaced the
same way.

Release whatever is being replaced, and allow only one reconnect loop at a
time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 12:32:06 -04:00
Brandon McGintyandClaude Opus 5 da1c6bdc26 release jitter buffer entries as they are consumed
Packets are removed from the jitter buffer by resliceing past them, which
leaves the popped entries in the backing array. Each one pins a decoded
audio frame until the array is next reallocated.

Clear the slot before resliceing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 12:31:25 -04:00
Brandon McGintyandClaude Opus 5 7647fdb233 release per-user audio resources before tearing down the renderer
Destroy detached the audio listener, which closes each per-user stream
channel and ends the goroutines OnAudioStream started, and then immediately
destroyed the OpenAL context and closed the renderer without waiting for
them. Those goroutines delete their OpenAL source and buffers through
s.render, which returns false once the renderer is closed, so the deletes
were silently skipped and the resources were left to the device.

Track the goroutines and let them finish first, bounded so a wedged renderer
cannot hang a reconnect. Both device close calls now report failure as well:
alcCloseDevice frees nothing and returns false while a device still has
contexts, buffers or sources outstanding, and dropping the handle after that
leaks the device and everything it owns somewhere the Go collector cannot
see it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 12:31:16 -04:00
Brandon McGintyandClaude Opus 5 82ecf713ff keep audio listeners reachable after a detach
AudioListeners.Attach set the tail pointer only when the list was empty, so
tail stayed on the first item ever attached. Once anything detached, the next
attach linked itself onto a node that was no longer in the list and that
listener never received audio again.

The equivalent code in Listeners.Attach is correct; this one had diverged
from it. The reconnect path detaches and re-attaches on every cycle, so this
was reachable in normal use.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 12:31:03 -04:00
Brandon McGintyandClaude Opus 5 f03d576604 reject an out-of-range tunnelled audio length
The length is taken from the packet and only checked against the upper
bound, so a negative value passed the check and then panicked on the slice
expression that follows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 12:30:56 -04:00
Brandon McGintyandClaude Opus 5 1e73d192ba reject a channel parent that is its own descendant
handleChannelState applied whatever parent the server named, so a channel
could be made its own ancestor. That leaves Parent/Children cyclic, and
everything that walks the channel tree afterwards recurses until it exhausts
memory.

Ignore such a move and keep the existing parent rather than corrupting the
graph. The ancestry walk is itself bounded so a graph that is already cyclic
cannot hang the check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 12:30:44 -04:00
Brandon McGintyandClaude Opus 5 07c923c4c9 bound the tree rebuild against a cyclic channel graph
rebuild_rec followed parent/child links with no depth limit, so a channel
graph in which a channel is its own ancestor recursed until the process ran
out of memory.

Cap the depth and give the whole rebuild a single node budget. Accumulating
into one slice rather than returning a new slice per node makes that budget
apply to the tree as a whole instead of to each level, and removes an
allocation per node.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 12:30:29 -04:00
Brandon McGintyandClaude Opus 5 314b838e33 bound the chat scrollback and wrap only new lines
AddLine re-wrapped every stored line on each append, and the buffer it
re-wrapped had no upper bound, so the work a session did grew as the square
of its length. Wrapping also built each display line by concatenating one
rune at a time, reallocating per character.

Wrap just the line being added, build it with a strings.Builder, and cap the
retained history. Trimming is done a block at a time because it forces a
rebuild; discarding a single line per append would re-wrap the whole buffer
again.

Measured over the same benchmark, 4000 lines went from 42s and 18.8GB
allocated to 4ms and 1MB, and 20000 lines now complete in 85ms.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 12:30:23 -04:00
Brandon McGintyandClaude Opus 5 0c8eec1bf6 rewrite barnard-ui in Python
Replace the shell implementation with Python while keeping the same
dialog-based interface and the same file layout under
~/.config/barnard.
The shell version parsed servers.conf with string operations that lost
whitespace and silently discarded entries it could not read, and every
new feature meant more quoting and subshell handling.

Refuse to start on a servers.conf we cannot interpret.
Unknown keys, stray sections, and invalid ports now name the file and
line instead of being dropped and rewritten over the top of the user's
file. Duplicate server names are reported rather than silently
collapsed.

Keep a backup of the previous server list and flush it to disk.
A save is written to a temporary file, the old one is copied to .bak,
and both the file and its directory are synced before the replace.

Confirm before overwriting or removing a server.

Bracket IPv6 addresses when building the -server argument.
The shell version joined address and port with a colon, which is not a
usable address for an IPv6 literal.

Take the hostname from os.uname rather than the environment.
HOSTNAME is a shell variable and is not exported, so the connect
username lost its host part.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 14:42:52 -04:00
Brandon McGintyandClaude Opus 5 e64c6df2b7 document the UDP protocol, Windows status, and new options
Add UDP_ENCRYPTION.md describing the OCB2 crypt state, the 1.5 native
protobuf envelope, nonce handling, and the version negotiation that
selects between the 1.5 and legacy payload formats.
The protocol is only described across several Mumble source files, and
getting the nonce direction wrong is not visible as anything except
audio that never arrives.

Add WINDOWS.md recording what builds there today and what does not.

Update the README for the new command line options and the tone test
mode.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 14:42:52 -04:00
Brandon McGintyandClaude Opus 5 3dc77cedd6 add audio tests against a live Mumble server
Add integration tests behind a build tag that connect to a local
Mumble server and check the audio path end to end.
Unit tests cover the codec and the packet format separately, but
neither catches a mismatch that only appears against a real server.
These send a known tone and verify the frequency, packet ordering, and
sequence continuity that come back, at 10 ms and longer intervals, over
native UDP and over the TCP tunnel.

Add a reference-vector test for the 1.5 protobuf envelope carried
inside the TCP tunnel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 14:42:52 -04:00
Brandon McGintyandClaude Opus 5 b77a491ec9 update Go dependencies
Move opus, go-toml, protobuf, go-runewidth, and golang.org/x/net to
their current releases.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 14:42:52 -04:00
Brandon McGintyandClaude Opus 5 d02177af71 log the incoming audio path at packet level
Record decoder creation, sequence numbers, frame lengths, and decode
results for each tunneled audio packet, and note when a slow listener
has a packet dropped.
Diagnosing a codec or ordering problem previously meant adding print
statements and rebuilding. These sit at info and debug, so they cost
nothing unless -logfile is given.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 14:42:52 -04:00
Brandon McGintyandClaude Opus 5 872149c977 run all terminal work on the UI goroutine and lock shared client state
Route network and audio callbacks through a bounded UI queue.
Protocol handlers, the audio thread, and key handlers all drew to
termbox widgets directly, which is a data race against the render
loop. postUI is now the only path from a callback to a widget, work is
dropped during shutdown, and the queue never blocks the caller.

Guard the mutable client state with mutexes.
Connection and transmission flags, the selected user, the muted
channel set, and the audio stream pointer were each read and written
from at least two goroutines. Lookups that walk the client's user and
channel maps take the client lock and copy what they need.

Snapshot the display string when a tree item is built.
Tree items held live pointers and formatted themselves during
rendering, so a user removed between rebuild and draw was dereferenced
on the render path.

Cancel reconnect retries on shutdown and release audio before
reconnecting.
Quitting during a retry left the goroutine sleeping until its timer
expired, and a reconnect built a second OpenAL stream on top of the
first. Cleanup is idempotent so repeated disconnect events are safe.

Make UI shutdown idempotent and join the event poller.
Close could be called more than once, and the polling goroutine could
be left trying to deliver an event after Run had returned. Run now
also reports a termbox initialization failure instead of returning nil.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 14:42:52 -04:00
Brandon McGintyandClaude Opus 5 a564286402 accept IPv6 server addresses without a port
Append the default port only when the address does not already have
one.
The check looked for any colon, so a bare IPv6 literal such as
2001:db8::1 was treated as already carrying a port and was passed
through unusable, while a bracketed literal without a port never got
one appended.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 14:42:52 -04:00
Brandon McGintyandClaude Opus 5 ecca0a63ed add a tone test mode that bypasses the soundcard
Add -tone-test to transmit a generated 440 Hz Opus tone and save
incoming audio to the file named by -tone-out.
Diagnosing an audio problem previously required a working capture and
playback device, which is exactly what is in question. This mode opens
no OpenAL device at all, so the network and codec path can be tested
on a machine with no sound hardware.

Open the capture file before starting the generator.
Starting transmission first left a tone goroutine running with nowhere
to write when the path was unusable.

Refuse file playback while in tone test mode, and ignore the
microphone volume keys.
Both operate on a stream that does not exist here.

Stop the generator and detach the file saver on disconnect.
A reconnect otherwise attached a second saver to the same file.

Add -auto-transmit to key the microphone as soon as the connection is
up.
Useful for a bot or a monitoring client that should never need a
keypress. It runs from both the connect event and the point where the
audio stream is created, because the server's welcome arrives before
the stream exists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 14:42:52 -04:00
Brandon McGintyandClaude Opus 5 17a4662c6c strip terminal control sequences from server-supplied text
Remove control, DEL, and bidi characters before display.
HTML escaping was the only filter, which leaves ANSI and OSC escape
sequences intact. A remote user could move the cursor, recolour the
screen, or reorder what was shown by putting escape codes in a
message, a nickname, or a channel name. Names rendered in the channel
tree go through the same filter now.

Keep the text box cursor and the prompt on rune boundaries.
Both indexed by byte, so editing a line containing multi-byte
characters could split one and render replacement characters.

Handle text view lines that carry no timestamp.
Toggling timestamps assumed every stored line had one and sliced past
the end of lines that did not.

Ignore key events on an empty tree.
The handlers indexed the item list before checking that it had any
items.

Remove the beep helpers.
They shelled out to an optional "beep" binary and panicked when it was
absent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 14:42:52 -04:00
Brandon McGintyandClaude Opus 5 97ec48534e bound notification delivery and expand placeholders once
Give the notification queue a buffer and drop events when it is full.
The channel was unbuffered, so a slow or hung notification command
blocked whichever UI or network callback happened to raise the event.

Expand the command placeholders in a single pass.
Substituting %event, then %who, then %what meant text arriving in an
earlier field could contain a later placeholder and have it expanded,
letting a remote user inject their own text into the command.

Move the command runner behind a build tag and drop the POSIX default
on Windows.
The helper script it pointed at does not exist there, so the default
was a command that could only fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 14:42:52 -04:00
Brandon McGintyandClaude Opus 5 4f41dd4ed6 harden and platformize FIFO handling
Reject a FIFO path that points to an existing non-FIFO object.
Previously we removed whatever was at that path before creating the
pipe, so pointing -fifo at a regular file silently deleted it. An
existing FIFO is reused and only a missing path is created.

Move the implementation into fifo_unix.go and fifo_windows.go.
This lets the Unix build use syscall.Mkfifo directly, which is not
available on Windows.

Prevent the reader from endlessly spinning on a closed FIFO after an
error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 14:42:52 -04:00
Brandon McGintyandClaude Opus 5 c5baaae6a7 stop file playback processes and their children reliably
Kill ffmpeg's whole process group rather than just the process.
ffmpeg spawns helpers for some inputs, so stopping playback left them
running and holding the output pipe open. The player now starts the
command in its own process group and signals the group.

Wait for the playback worker to finish before starting a new file.
Playing a second file while the first was shutting down left two
workers writing to the same stream.

Make pausing nonblocking.
The pause path could block on a full pipe and hang the UI thread that
requested it.

Install and reset the stereo encoder under the client lock.
File playback swapped the encoder field directly while a voice frame
could be encoding with it, and a finished file left the encoder
carrying state into the next one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 14:42:52 -04:00
Brandon McGintyandClaude Opus 5 fbb6a148ff restore the saved microphone volume on connect
Apply the persisted microphone volume when the stream is created.
The value was written to the configuration on every adjustment but
never read back, so the microphone returned to full gain on each
start.

Read a saved volume of zero as zero rather than as a missing value.
A user who muted their microphone and quit came back unmuted.

Save the configuration after a volume change and report a failure.
The setter only updated the in-memory value, so the new level was lost
unless something else happened to save afterwards.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 14:42:52 -04:00
Brandon McGintyandClaude Opus 5 cb4f91596e make microphone AGC less aggressive and switchable
Lower the gain ceiling and slow the attack of the automatic gain
control.
It amplified room noise between words hard enough to be audible as a
rising hiss whenever the speaker paused.

Add an AGC toggle on F12 and an /agc command.
AGC has always been applied unconditionally, which is wrong for a
microphone that is already levelled by hardware or by the system
mixer. The preference is saved and reapplied on connect, defaulting to
on so existing setups are unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 14:42:52 -04:00
Brandon McGintyandClaude Opus 5 17173b779b report configuration failures instead of crashing
Return errors from the save path rather than panicking.
Every write panicked on failure, so a read-only or missing
configuration directory killed a running client. The parent directory
is created when absent, and callers now show the failure in the output
window and carry on.

Write through an unpredictable temporary file.
The old fixed ".tmp" name next to the configuration was a symlink
target an attacker could plant in advance.

Serialize configuration reads and writes.
Hotkey handlers, the audio thread, and the connection callbacks all
touch the same structure, so saves could interleave with updates.

Parse addresses with SplitHostPort.
Splitting on every colon broke IPv6 addresses and panicked outright on
an address with no port. Both now fall back to Mumble's default port.

Fail immediately when an explicitly requested config file is missing
or is not a regular file.
Silently falling back to defaults hid a mistyped -config path.

Supply defaults for the clear-output and scroll-to-top and -bottom
hotkeys.
The UI registered listeners for them but the configuration never
filled the keys in, so the bindings were nil and the keys did nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 14:42:52 -04:00
Brandon McGintyandClaude Opus 5 7e1faba06b make the outgoing packet interval configurable
Add -audio-interval to choose a 10, 20, 40, or 60 ms packet duration.
Longer packets cut per-packet overhead on slow or lossy links at the
cost of latency. Anything else is rejected at startup rather than
silently truncated to 10 ms frames.

Step the audio sequence by the frame duration.
Sequence numbers are Mumble timestamps in 10 ms units, so a 60 ms
packet advances the counter by six. Incrementing by one made the
receiver see every packet as arriving far too early.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 14:42:52 -04:00
Brandon McGintyandClaude Opus 5 6dcaa7f7a6 handle dropped packets and network delays
Add a per-user jitter buffer with restart resync.
Incoming audio was rendered straight to OpenAL as packets arrived, so
normal network jitter produced gaps and the renderer starved between
frames. Buffer each user's decoded frames for a configurable playout
delay (-jitter-buffer, default 40ms) and let the render thread pull
from that buffer instead.

Drop late packets, and use packet duration to set the expected frame
number.
A packet below the expected frame number is dropped rather than
stalling delivery, and the expected frame is stepped by the packet's
real duration instead of a fixed 10ms.

After detecting a large backward jump in frame numbering, along with a
sustained run of late packets, resync to the new frame numbering.
When a sender switches audio devices, Mumble destroys and recreates
their audio stream. This resets their frame number to zero without
sending a terminator packet, which causes a potentially endless hang
while we wait for a frame number that will not arrive for hours.

Own the OpenAL context on one dedicated render thread.
Contexts are current to an OS thread, so creating sources and buffers
from whichever goroutine happened to be running could operate on no
context at all. Every source and buffer is now created, filled, and
deleted on that thread, and file playback renders through it too.

Report device and capture failures with the device name.
Startup errors said only that a device could not be opened, and a
capture stall was reported as a microphone failure even though it is
normal scheduler timing. Fatal microphone errors now exit instead of
leaving a client that cannot transmit.

Release queued buffers and deactivate the context on shutdown.
Streams were torn down while buffers were still queued on a source,
and the context was destroyed while still current.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 14:42:46 -04:00
Brandon McGintyandClaude Opus 5 5ec82eb1fd fix recorded audio truncation and rate mismatch
Accumulate incoming frames per source and consume fixed-size chunks.
Each speaker's frames were queued whole and one frame was taken per
tick, so a frame that did not match the recorder's frame size was
truncated or padded and the recording drifted out of time with the
audio. Frames of any size now append to a per-source buffer that is
drained in exact chunks.

Tell the recorder whether a frame is stereo instead of guessing from
its length.
Mono microphone frames were being interpreted as stereo, which halved
their duration and produced static in the output.

Let the recorder worker close ffmpeg's stdin.
Stop closed it from the caller while the worker was still writing,
turning a normal stop into a broken pipe and losing the tail of the
recording.

Reserve the output file exclusively and hand ffmpeg the descriptor.
The path was generated, then reopened by name, so another process
could take the name in between. The file is opened once with O_EXCL
and passed to ffmpeg as an inherited descriptor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 14:42:41 -04:00
Brandon McGintyandClaude Opus 5 af37bcd5d6 guard the OpenAL bindings against empty slices and untyped handles
Return early instead of indexing empty slices.
Buffer, source, listener, and capture calls took the address of
element zero to hand C a pointer, which panics when the caller passes
nothing to delete, queue, or read.

Give devices and contexts their own handle types.
They were passed around as untyped pointers, so a device could be
supplied where a context was expected and the mistake only showed up
as a crash inside the C library.

Delete buffers through the buffer API.
Buffer names were being freed with the source deletion call, which
leaks the buffer and can free an unrelated object.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 14:25:55 -04:00
Brandon McGintyandClaude Opus 5 e41d2fd8cb validate audio configuration before connecting
Reject unsupported audio intervals and non-positive buffer sizes.
An interval like 15ms was truncated to 10ms frames while the send
ticker kept the original duration, so packets were produced at a rate
the frame size did not match. Checking at dial time gives a clear
error instead of malformed audio.

Require all three coordinates for positional audio.
Only X was checked, so supplying X without Y or Z wrote a header that
claimed positional data and then read past the values that were
actually provided.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 14:25:35 -04:00
Brandon McGintyandClaude Opus 5 1a6c13e8aa verify TLS certificates against the server name
Derive the TLS server name from the configured address.
tls.DialWithDialer was handed the raw address, so a server reached by
a name that differs from its certificate could not be verified and
SNI was never sent. The dial is now split into a plain TCP connect and
an explicit tls.Client with the hostname filled in.

Clone the caller's TLS configuration before modifying it.
Reconnects and concurrent clients share one configuration value, and
setting ServerName on it would leak across connections.

Apply the dialer timeout to the handshake as well.
net.Dialer.Timeout covers only the TCP connect, so a peer that accepts
and then goes silent could block startup forever.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 14:25:08 -04:00
Brandon McGintyandClaude Opus 5 3787ad4cd1 send audio over native UDP with OCB2 encryption
Add the OCB2-AES128 crypt state used by the Mumble UDP channel.
CryptSetup carries the key and both nonces; the handler installs them
instead of being an unimplemented stub, so UDP audio can be encrypted
and decrypted at all.

Add the Mumble 1.5 native UDP transport.
The socket opens during the handshake so it is ready when CryptSetup
arrives, and audio is sent as a protobuf envelope with a leading type
byte. Advertise ourselves as a 1.5 client so servers select it.

Advance the decrypt IV from the server nonce.
Using the client nonce for inbound packets desynchronized the two
sides after the first packet.

Keep the legacy UDPVoiceOpus payload working inside 1.5 crypto.
A 1.5 server still tunnels the older 0x80 format to older peers, so
both payload shapes have to be decoded from the same envelope.

Fall back to the TCP tunnel until a UDP packet is authenticated.
Nothing confirms the return path works until the server answers, so
audio stays tunneled until then and switches to UDP afterwards.
Tunneled packets are ignored while UDP is active so a frame is never
processed twice.

Add -tcp to force the tunnel and skip UDP entirely.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 14:24:44 -04:00
Brandon McGintyandClaude Opus 5 aa1d233c81 conceal lost audio and size the Opus encoder to the frame
Fill sequence gaps with Opus packet loss concealment.
A dropped packet previously left a hole in the stream. The decoder is
now asked for a concealed frame for each missing sequence number, and
is only reset when a decode actually fails or a talk burst ends.

Mark the end of a talk burst with a terminator packet.
Listeners need to drop their per-speaker ordering state before the
sender starts numbering a new burst.

Set the encoder bitrate from the packet budget and frame size.
SetBitrateToMax ignored the server's bandwidth limit, so frames were
encoded larger than the allowed data bytes and truncated. The result
is clamped to Opus's 8 kbps floor, and the auto-bitrate listener now
keeps at least ten bytes per frame so a low-bandwidth server cannot
compute a budget too small to encode anything.

Allocate the decode buffer for the sample count it is given.
The decoder doubled the requested frame size for stereo and then
returned a slice scaled by the same factor again.

Hold the client read lock across encode and reset.
File playback can replace or reset the stereo encoder while a voice
frame is being encoded with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 14:23:46 -04:00
Brandon McGintyandClaude Opus 5 60470ad091 make per-user audio state thread-safe
Move volume, boost, mute, and OpenAL source behind accessors guarded
by a per-user mutex.
The audio goroutine reads these fields on every decoded packet while
the UI writes them from key handlers, which is a data race on the
gain a stream is currently rendering with.

Also add the per-user sequence fields the decoder needs to notice
gaps in a user's audio stream.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 14:22:33 -04:00
Brandon McGintyandClaude Opus 5 b67940ddbc reject malformed protocol records and stale admin targets
Reject ACL groups and user-list entries that omit required fields.
Both handlers dereferenced optional protobuf pointers directly, so a
malformed or hostile packet crashed the client.

Read user statistics from the server counters.
The three FromServer fields were guarded by the matching FromClient
pointers, so server-side late, lost, and resync counts were reported
as zero whenever the client-side ones were absent.

Remove stale reverse links when a channel's link set is replaced.
Rebuilding the map left the other channel still pointing back at us,
and the new links were never made reciprocal.

Clamp negative ban durations to zero.
Duration is sent as an unsigned protocol field, so a negative value
became an effectively permanent ban.

Validate manual ban minutes before they reach that field.
Reject non-numeric, negative, and overflowing input in the UI rather
than converting it silently.

Confirm admin targets still exist before running an action.
A user or channel can be removed by the server while an admin prompt
is open, leaving the action pointed at an object no longer in the
connection.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 14:21:04 -04:00
Brandon McGintyandClaude Opus 5 28b026c90f dispatch events and audio from locked snapshots
Guard the listener lists with a mutex and iterate a snapshot.
Attaching or detaching a listener from another goroutine mutated the
linked list while a dispatch was walking it, and a second Detach on
the same item corrupted the head and tail links.

Deliver audio through dispatchAudio instead of an inline loop that
juggled the volatile lock.
The old loop unlocked and relocked around every listener, so a user
map change mid-dispatch could be missed. Streams are now buffered and
a slow listener has its packet dropped rather than blocking protocol
processing.

Close a user's audio streams when the user is removed.
Detaching a listener closes its streams too, so a stream can no longer
be written after the reader is gone.

Unlock volatile when a user moves to an unknown channel.
The error path locked it a second time instead of unlocking, which
deadlocked all later protocol handling.

Give context actions their client when they are created.
Every context action method dereferences it, so triggering a
server-supplied action panicked on a nil client.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 14:20:11 -04:00
Brandon McGintyandClaude Opus 5 4788f8da24 correct varint encoding for the 1.5 UDP protocol
Encode values the way protobuf does rather than with Mumble's
tunnel-specific varint format.
The 1.5 native UDP protocol carries protobuf messages, so the two
encodings were being mixed and the server rejected our packets.

Handle the minimum signed 64-bit value without overflowing.
Negating math.MinInt64 wraps back to itself, which produced a
corrupt encoding instead of an error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 14:18:17 -04:00
Brandon McGintyandClaude Opus 5 995ee1bffc add opt-in leveled logging
Add a log package with debug, info, warn, and error levels.
It wraps a single process-wide logger that the gumble and audio
packages can call into without importing a logging library.

Logging is off unless -logfile names a file.
Anything written to stderr while the terminal UI is running corrupts
the display, so a logger is only installed once an explicit
destination exists. -log sets the level and defaults to warn.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 14:18:01 -04:00
Brandon McGinty 5e941be8cb barnard-ui: fix hostname 2026-08-20 13:11:23 -04:00
Brandon McGinty (deepseek) d35dda68fc Fix dialog display for yesno and msgbox prompts 2026-08-19 15:19:28 -04:00
Brandon McGinty (deepseek) 0098afc595 Rewrite barnard-ui in Python with strict config handling 2026-08-19 15:19:28 -04:00
Brandon McGinty (deepseek) 2cdbce8edb Show enable/disable logs and pass debug logging to barnard 2026-08-14 12:32:41 -04:00
Brandon McGinty (deepseek) d2dff7f521 Add barnard-ui session logging and wormhole log sharing 2026-08-14 12:29:42 -04:00
Brandon McGinty (chatgpt) c0ef036934 Add configurable incoming audio jitter buffer 2026-08-13 17:54:30 -04:00
Brandon McGinty (chatgpt) d338925da6 Add outgoing audio packet interval option 2026-08-13 16:00:30 -04:00
Brandon McGinty (chatgpt) 893908f9a1 Reset audio jitter state at talk burst end 2026-08-13 13:37:47 -04:00
Brandon McGinty (chatgpt) 05dc6e4e0e Set TLS server name from Mumble address 2026-08-13 12:53:35 -04:00
Brandon McGinty (chatgpt) 883f7250f5 Avoid debug logging induced UDP loss 2026-08-12 17:38:04 -04:00
Brandon McGinty (chatgpt) 4510c25350 Recover UDP crypto after packet loss 2026-08-12 16:30:38 -04:00
Brandon McGinty (deepseek) 90f8a1ca5f Fix recording truncation and rate mismatch causing static
The recording pipeline had two bugs that combined to produce
buzzing/static in recorded audio:

1. NormalizeStereoFrame truncated incoming audio frames to
   frameSize*AudioChannels samples. When the Opus decoder produced
   20ms frames (1920 stereo samples) but the recorder used a 10ms
   frameSize (960 samples), half the audio from every packet was
   silently dropped.

2. The recorder's run() loop dequeued one fixed-length frame per
   source per tick. After truncation, the remaining 10ms of each
   20ms packet was gone, so every other tick produced silence.
   This 50 Hz on/off pattern sounded like static.

Fixes:
- NormalizeStereoFrame no longer truncates; it only converts mono
  to stereo and preserves all audio data
- RecordAudioFrame now accepts an explicit stereo flag from callers
  instead of guessing from sample count (which failed for even-length
  mono data like 480-sample mic frames)
- run() accumulates variable-length frames per source and consumes
  them in fixed-size chunks, preserving any leftover for the next tick
2026-08-10 18:34:52 -04:00
Brandon McGinty (deepseek) cd003d7ac4 Fix jitter buffer: late-packet blockage and frame-step tracking
- popNext() now computes the actual frame step from PCM sample count
  instead of always advancing by 1. Mumble 1.5 frame numbers are 10ms
  timestamps, so 20ms stereo frames advance by 2, eliminating the
  'seq gap' skip that fired on every single packet.

- Drain loop now discards late/duplicate packets (sequence < expected)
  instead of letting them permanently block the buffer. Previously a
  single late packet at jitterBuf[0] would cause popNext to always
  return nil without the gap check catching it (only handled >), so
  all subsequent drains would break immediately.

- Reduced reclaim log verbosity: log every 50th or on state change,
  not every call with processed==0.
2026-08-10 18:11:26 -04:00
Brandon McGinty (deepseek) 3613a42fce Add debug logging for OpenAL playback and jitter buffer pipeline
- Log OpenAL vendor/version/renderer at startup
- Log buffer creation count and user volume on new audio stream
- Enhanced reclaim(): log source state, processed/queued/empty counts,
  and check for OpenAL errors; log every 50th cycle or on unusual state
- Log jitter buffer drain events (first 3 + periodic) with buffer counts
- Log sequence gap skips in jitter buffer
- WARN when processAudioPacket has no empty buffers (audio dropped)
- Check OpenAL errors after Buffer.SetData, QueueBuffer, and Play()
- Log source state transitions when calling Play() on non-playing source
2026-08-10 18:03:43 -04:00
Brandon McGinty fd6b7514bf config ignore 2026-08-10 16:12:14 -04:00
Brandon McGinty (chatgpt) 99f1f4b9ab Keep OpenAL jitter playback continuous 2026-08-10 16:07:47 -04:00
Brandon McGinty (chatgpt) 5b0879ddaa Fix UDP audio frame timestamp handling 2026-08-10 15:18:04 -04:00
Brandon McGinty (chatgpt) 9c487c2586 Add Windows platform integration scaffolding 2026-08-10 12:36:47 -04:00
Brandon McGinty (chatgpt) fab4fd50d2 Test exclusive tone-test output creation 2026-08-10 11:25:52 -04:00
Brandon McGinty (chatgpt) e9ba7f580a Synchronize connection-owned stream access 2026-08-10 11:25:08 -04:00
Brandon McGinty (chatgpt) f0aa5ff35a Update Go dependencies 2026-08-10 11:25:08 -04:00
Brandon McGinty (chatgpt) ef8a92149d Report configuration persistence failures 2026-08-10 11:25:08 -04:00
Brandon McGinty (chatgpt) 655a109ce8 Create tone-test outputs exclusively 2026-08-10 11:25:08 -04:00
Brandon McGinty (chatgpt) 65489fd367 Prevent recording output path replacement 2026-08-10 11:25:08 -04:00
Brandon McGinty (chatgpt) 57961ffc9f Synchronize connection-owned audio resources 2026-08-10 10:28:01 -04:00
Brandon McGinty (chatgpt) b407a20132 Snapshot mutable tree display state 2026-08-10 10:27:12 -04:00
Brandon McGinty (chatgpt) 461f172035 Validate admin targets before actions 2026-08-10 10:26:03 -04:00
Brandon McGinty (chatgpt) cf9e1c6d0a Terminate file playback process groups 2026-08-10 10:25:25 -04:00
Brandon McGinty (chatgpt) 1d6ab79ed0 Cancel reconnect retries during shutdown 2026-08-10 10:24:38 -04:00
Brandon McGinty (chatgpt) e68546eec5 Compare UDP authentication tags in constant time 2026-08-10 09:42:49 -04:00
Brandon McGinty (chatgpt) 9fdcf6171d Reserve recording paths atomically 2026-08-10 08:55:25 -04:00
Brandon McGinty (chatgpt) 43d7addc15 Protect regular files from FIFO setup 2026-08-10 08:50:56 -04:00
Brandon McGinty (chatgpt) dcffa1efa1 Use secure temporary config files 2026-08-10 08:44:58 -04:00
Brandon McGinty (chatgpt) 3ba5e03208 Restore output navigation hotkeys 2026-08-10 08:26:37 -04:00
Brandon McGinty (chatgpt) 95c7394b01 Snapshot context actions under client lock 2026-08-10 07:31:18 -04:00
Brandon McGinty (chatgpt) 7785f0a5b5 Synchronize audio encoder selection 2026-08-10 05:03:45 -04:00
Brandon McGinty (chatgpt) d399650198 Protect crypto initialization checks 2026-08-10 04:28:38 -04:00
Brandon McGinty (chatgpt) fa84373a81 Synchronize legacy UDP crypto setup 2026-08-10 04:06:12 -04:00
Brandon McGinty (chatgpt) 42861dfcd5 Close audio streams when listeners detach 2026-08-10 04:01:42 -04:00
Brandon McGinty (chatgpt) 2eed3c809f Synchronize audio stream teardown 2026-08-10 03:35:35 -04:00
Brandon McGinty (chatgpt) e8e2fc46a9 Reject overflowing manual ban durations 2026-08-10 01:11:58 -04:00
Brandon McGinty (chatgpt) 55772fa459 Synchronize connection and transmission state 2026-08-10 00:45:22 -04:00
Brandon McGinty (chatgpt) 3efbd019ce Synchronize selected user access 2026-08-10 00:42:09 -04:00
Brandon McGinty (chatgpt) 20c6997ab0 Synchronize muted channel state 2026-08-10 00:31:29 -04:00
Brandon McGinty (chatgpt) 118799d112 Serialize configuration updates and writes 2026-08-10 00:18:42 -04:00
Brandon McGinty (chatgpt) 874cbb7818 Classify recursive channel messages as public 2026-08-10 00:00:40 -04:00
Brandon McGinty (chatgpt) d7dcaa8b21 Support IPv6 server addresses without ports 2026-08-09 23:55:51 -04:00
Brandon McGinty (chatgpt) 770635955a Sanitize terminal widget text 2026-08-09 23:54:49 -04:00
Brandon McGinty (chatgpt) 8348a7083d Fix auto transmit and server messages 2026-08-09 23:28:50 -04:00
Brandon McGinty (chatgpt) 219acad951 Reject file playback in tone-test mode 2026-08-09 23:14:38 -04:00
Brandon McGinty (chatgpt) 1081d894c9 Avoid stream access from tone-test tree controls 2026-08-09 23:14:38 -04:00
Brandon McGinty (chatgpt) df185cc9f5 Queue status updates from network callbacks 2026-08-09 23:13:20 -04:00
Brandon McGinty (chatgpt) bad4e8172a Detach tone-test savers on reconnect 2026-08-09 23:12:56 -04:00
Brandon McGinty (chatgpt) b23afebdb3 Reject non-regular explicit config files 2026-08-09 23:11:31 -04:00
Brandon McGinty (chatgpt) 6da5b249f9 Reject missing explicit configuration files 2026-08-09 22:35:38 -04:00
Brandon McGinty (chatgpt) 1ea71b1862 Require talk key for tone test transmission 2026-08-09 22:31:37 -04:00
Brandon McGinty (chatgpt) 612db896ce Show active transmission in tone test mode 2026-08-09 22:28:52 -04:00