Files
barnard/gumble/gumbleopenal/stream_regression_test.go
Tyler SpiveyandClaude Opus 5 132df6863a Resync jitter buffer when a sender restarts frame numbering
Mumble destroys and recreates AudioInput when the sender switches audio
devices. The destructor sends no terminator, and the replacement resets
iFrameCounter to zero, so a sender who never unkeys silently restarts its
frame numbering mid-burst. The jitter buffer kept expecting the old
sequence and discarded every packet as late until the next unkey.

Resync on a sustained run of late packets combined with a backwards jump
too large to be network reordering. Both conditions are needed: the run
length alone would let a clump of reordered packets drag the expected
sequence backwards, and small jumps need no intervention because the
restarted stream climbs past the stale expectation on its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 13:00:06 -07:00

101 lines
3.5 KiB
Go

package gumbleopenal
import (
"errors"
"strings"
"testing"
"time"
"git.stormux.org/storm/barnard/gumble/go-openal/openal"
"git.stormux.org/storm/barnard/gumble/gumble"
)
// Regression: audio cleanup could send a final render command after Destroy
// had closed renderCh, panicking instead of safely discarding that work.
// Regression: StopSource returned before the capture worker ended, allowing
// Destroy to close the device while that worker still used it.
// Regression: OpenAL returned only a generic input/output error, hiding the
// actual configured device that a user must correct.
func TestDeviceOpenErrorsIncludeConfiguredDevice(t *testing.T) {
input := openInputDeviceError("virtual_mic.monitor", openal.FormatMono16)
if !errors.Is(input, ErrInputDevice) || !strings.Contains(input.Error(), "virtual_mic.monitor") {
t.Fatalf("input error %q", input)
}
output := openOutputDeviceError("")
if !errors.Is(output, ErrOutputDevice) || !strings.Contains(output.Error(), "default") {
t.Fatalf("output error %q", output)
}
}
// Regression: later capture start failures also omitted the configured device.
func TestStartSourceUnavailableDeviceIncludesName(t *testing.T) {
s := &Stream{inputDeviceName: "virtual_mic.monitor"}
err := s.StartSource(nil)
if !errors.Is(err, ErrMic) || !strings.Contains(err.Error(), "virtual_mic.monitor") {
t.Fatalf("start error %q", err)
}
}
func TestStopSourceWaitsForWorker(t *testing.T) {
stop, done := make(chan bool), make(chan struct{})
s := &Stream{sourceStop: stop, sourceDone: done}
returned := make(chan struct{})
go func() { _ = s.StopSource(); close(returned) }()
select {
case <-returned:
t.Fatal("StopSource returned before worker")
default:
}
close(done)
<-returned
}
func TestJitterPlaybackDelayAppliesOnlyAtStartup(t *testing.T) {
if jitterPlaybackReady(false, 20*time.Millisecond, 40*time.Millisecond) {
t.Fatal("jitter playback started before initial buffer filled")
}
if !jitterPlaybackReady(false, 40*time.Millisecond, 40*time.Millisecond) {
t.Fatal("jitter playback did not start after initial buffer filled")
}
if !jitterPlaybackReady(true, 0, 40*time.Millisecond) {
t.Fatal("jitter playback paused while refilling after startup")
}
}
func TestJitterResyncsAfterSenderRestartsSequence(t *testing.T) {
// Mumble restarts frame numbering at zero when the sender switches audio
// devices mid-burst, and sends no terminator to announce it.
if !jitterShouldResync(jitterLateResync, 52724) {
t.Fatal("jitter did not resync after the sender restarted its frame numbering")
}
if jitterShouldResync(jitterLateResync-1, 52724) {
t.Fatal("jitter resynced before the late run was conclusive")
}
// A clump of reordered packets is bounded and recovers on its own; it must
// not drag the expected sequence backwards.
if jitterShouldResync(jitterLateResync, jitterResyncJump-1) {
t.Fatal("jitter resynced on a backwards jump small enough to be reordering")
}
if jitterShouldResync(1, 52724) {
t.Fatal("jitter resynced on a single late packet")
}
}
func TestAudioPacketDurationUsesStereoFrameCount(t *testing.T) {
packet := &gumble.AudioPacket{AudioBuffer: make(gumble.AudioBuffer, 2*gumble.AudioDefaultFrameSize)}
if got := audioPacketDuration(packet); got != 10*time.Millisecond {
t.Fatalf("audioPacketDuration = %v, want 10ms", got)
}
}
func TestRenderRejectsWorkAfterShutdown(t *testing.T) {
s := &Stream{renderClosed: true}
called := false
if s.render(func() { called = true }) {
t.Fatal("closed renderer accepted work")
}
if called {
t.Fatal("closed renderer executed work")
}
}