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>
101 lines
3.5 KiB
Go
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")
|
|
}
|
|
}
|