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>
This commit is contained in:
Brandon McGinty
2026-08-20 14:42:46 -04:00
co-authored by Claude Opus 5
parent 5ec82eb1fd
commit 6dcaa7f7a6
4 changed files with 844 additions and 184 deletions
+19
View File
@@ -14,6 +14,7 @@ import (
"os/exec"
"strings"
"syscall"
"time"
barnlog "git.stormux.org/storm/barnard/log"
@@ -114,6 +115,7 @@ func main() {
serverSet := false
usernameSet := false
buffers := flag.Int("buffers", 16, "number of audio buffers to use")
jitterBuffer := flag.Int("jitter-buffer", 40, "incoming per-user audio buffer in ms (0, 20, 40, or 60)")
profile := flag.Bool("profile", false, "add http server to serve profiles")
noiseSuppressionEnabled := flag.Bool("noise-suppression", false, "enable noise suppression for microphone input")
tcpOnly := flag.Bool("tcp", false, "disable UDP, force audio through TCP tunnel")
@@ -121,6 +123,10 @@ func main() {
logFile := flag.String("logfile", "", "write logs to this file (logging is disabled when omitted)")
flag.Parse()
selectedJitterBuffer, err := jitterBufferDuration(*jitterBuffer)
if err != nil {
handle_raw_error(err)
}
// Set up logging
var level barnlog.Level
@@ -214,6 +220,7 @@ func main() {
}
b.Config.Buffers = *buffers
b.Config.DisableUDP = *tcpOnly
b.Config.IncomingAudioBuffer = selectedJitterBuffer
b.Hotkeys = b.UserConfig.GetHotkeys()
b.UserConfig.SaveConfig()
@@ -253,6 +260,18 @@ func main() {
handle_error(&b)
}
// jitterBufferDuration converts the requested incoming playout delay to a
// supported duration. Zero starts playback without an initial safety buffer.
func jitterBufferDuration(milliseconds int) (time.Duration, error) {
interval := time.Duration(milliseconds) * time.Millisecond
switch interval {
case 0, 20 * time.Millisecond, 40 * time.Millisecond, 60 * time.Millisecond:
return interval, nil
default:
return 0, fmt.Errorf("jitter buffer must be 0, 20, 40, or 60 ms, got %d", milliseconds)
}
}
func handle_raw_error(e error) {
fmt.Fprintf(os.Stderr, "%s\n", e.Error())
os.Exit(1)