diff --git a/gumble/gumbleopenal/stream.go b/gumble/gumbleopenal/stream.go index 5d061cb..46e3167 100644 --- a/gumble/gumbleopenal/stream.go +++ b/gumble/gumbleopenal/stream.go @@ -51,9 +51,17 @@ type Recorder interface { const recorderOutgoingSource uint32 = ^uint32(0) const ( - maxBufferSize = 11520 // Max frame size (2880) * bytes per stereo sample (4) + maxBufferSize = 11520 // Max frame size (2880) * bytes per stereo sample (4) + jitterMinPackets = 3 + jitterMaxPackets = 10 ) +// jitterPlaybackReady holds the initial playout delay only once. Requiring +// the minimum on every packet drains and refills the renderer in bursts. +func jitterPlaybackReady(started bool, buffered int) bool { + return started || buffered >= jitterMinPackets +} + var ( ErrState = errors.New("gumbleopenal: invalid state") ErrMic = errors.New("gumbleopenal: microphone disconnected or misconfigured") @@ -451,15 +459,10 @@ func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) { } // Jitter buffer: collects incoming packets, reorders by - // sequence number, and releases them in order after a small - // initial delay (3-5 packets / 30-50ms). - const ( - jitterMinPackets = 3 // minimum packets before playout starts - jitterMaxPackets = 10 // maximum buffered packets before dropping oldest - ) + // sequence number, and releases them after a small initial delay. var jitterBuf []*gumble.AudioPacket var jitterNextSeq int64 - var jitterInit bool + var jitterInit, jitterStarted bool // insertSorted inserts a packet into the jitter buffer sorted // by sequence number. @@ -509,10 +512,13 @@ func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) { jitterInit = true } - // Only start playing when we have enough buffered packets - if len(jitterBuf) < jitterMinPackets { + // Hold only the initial packets. Once playback starts, drain every + // ready packet so the renderer is fed continuously rather than in + // bursts of jitterMinPackets packets. + if !jitterPlaybackReady(jitterStarted, len(jitterBuf)) { continue } + jitterStarted = true // Drain all packets that are ready (in sequence order) for { diff --git a/gumble/gumbleopenal/stream_regression_test.go b/gumble/gumbleopenal/stream_regression_test.go index 9832178..690362f 100644 --- a/gumble/gumbleopenal/stream_regression_test.go +++ b/gumble/gumbleopenal/stream_regression_test.go @@ -48,6 +48,18 @@ func TestStopSourceWaitsForWorker(t *testing.T) { <-returned } +func TestJitterPlaybackDelayAppliesOnlyAtStartup(t *testing.T) { + if jitterPlaybackReady(false, jitterMinPackets-1) { + t.Fatal("jitter playback started before initial buffer filled") + } + if !jitterPlaybackReady(false, jitterMinPackets) { + t.Fatal("jitter playback did not start after initial buffer filled") + } + if !jitterPlaybackReady(true, 1) { + t.Fatal("jitter playback paused while refilling after startup") + } +} + func TestRenderRejectsWorkAfterShutdown(t *testing.T) { s := &Stream{renderClosed: true} called := false