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>
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>
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>
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>
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>
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>
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>
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>
Make the F11 action menu close through a shared close action so Escape and the explicit Close actions menu item follow the same path. Treat Escape-prefixed Up and Down events as close inputs while the action menu is active, which handles termbox InputAlt behavior after pressing Escape. Preserve and restore the user/channel tree selection across action menu open and close, and preserve selection across live tree rebuilds.
Tested with: GOCACHE=/tmp/barnard-go-cache go test ./...
Implements 7 voice effects that can be cycled through with F12:
- None (default)
- Echo: Single repeating delay with feedback (250ms)
- Reverb: Multiple short delays without feedback
- High Pitch: Chipmunk voice using cubic interpolation
- Low Pitch: Deep voice effect
- Robot: Ring modulation for robotic sound
- Chorus: Layered voices with pitch variations
The effects are applied after noise suppression and AGC in the audio
pipeline. Selected effect is persisted to config file. Includes
comprehensive documentation in README.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This addresses low microphone volume issues by automatically normalizing
outgoing audio levels with dynamic range compression and soft limiting.
The AGC is always enabled and applies voice-optimized parameters to
ensure consistent audio levels are sent to other users while preserving
manual volume control for incoming audio.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>