Fix jitter buffer: late-packet blockage and frame-step tracking

- popNext() now computes the actual frame step from PCM sample count
  instead of always advancing by 1. Mumble 1.5 frame numbers are 10ms
  timestamps, so 20ms stereo frames advance by 2, eliminating the
  'seq gap' skip that fired on every single packet.

- Drain loop now discards late/duplicate packets (sequence < expected)
  instead of letting them permanently block the buffer. Previously a
  single late packet at jitterBuf[0] would cause popNext to always
  return nil without the gap check catching it (only handled >), so
  all subsequent drains would break immediately.

- Reduced reclaim log verbosity: log every 50th or on state change,
  not every call with processed==0.
This commit is contained in:
Brandon McGinty (deepseek)
2026-08-10 18:11:26 -04:00
committed by Brandon McGinty
parent 3613a42fce
commit cd003d7ac4
+41 -10
View File
@@ -470,8 +470,8 @@ func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) {
emptyBufs = append(emptyBufs, reclaimedBufs...)
}
reclaimLogCounter++
// Log every 50th reclaim to avoid spam, but always log if state is unusual
if reclaimLogCounter%50 == 1 || processed == 0 || srcState != openal.Playing {
// Log every 50th reclaim, or if state is not Playing
if reclaimLogCounter%50 == 1 || srcState != openal.Playing {
log.Debug("reclaim #%d: state=%s processed=%d queued=%d empty=%d",
reclaimLogCounter, srcState, processed, queued, len(emptyBufs))
}
@@ -516,7 +516,26 @@ func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) {
}
p := jitterBuf[0]
jitterBuf = jitterBuf[1:]
jitterNextSeq = p.Sequence + 1
// Frame numbers are Mumble timestamps in 10 ms units.
// Compute the actual step from the PCM sample count so we
// never skip a legitimate gap.
samples := len(p.AudioBuffer)
if samples > gumble.AudioDefaultFrameSize && samples%2 == 0 {
// Stereo: step = stereo frames / base frame size
step := int64((samples / 2) / gumble.AudioDefaultFrameSize)
if step >= 1 {
jitterNextSeq = p.Sequence + step
} else {
jitterNextSeq = p.Sequence + 1
}
} else {
step := int64(samples / gumble.AudioDefaultFrameSize)
if step >= 1 {
jitterNextSeq = p.Sequence + step
} else {
jitterNextSeq = p.Sequence + 1
}
}
return p
}
@@ -548,13 +567,25 @@ func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) {
for {
pkt := popNext()
if pkt == nil {
// A locally dropped packet must not permanently stall the
// jitter buffer waiting for a sequence number that cannot arrive.
if len(jitterBuf) > 0 && jitterBuf[0].Sequence > jitterNextSeq {
log.Debug("jitter: seq gap for %s, skipping from %d to %d (buf=%d)",
e.User.Name, jitterNextSeq, jitterBuf[0].Sequence, len(jitterBuf))
jitterNextSeq = jitterBuf[0].Sequence
continue
if len(jitterBuf) > 0 {
if jitterBuf[0].Sequence < jitterNextSeq {
// Late or duplicate: discard so it doesn't
// permanently block the drain loop.
log.Debug("jitter: discarding late seq=%d for %s (next=%d buf=%d)",
jitterBuf[0].Sequence, e.User.Name, jitterNextSeq, len(jitterBuf))
jitterBuf = jitterBuf[1:]
continue
}
if jitterBuf[0].Sequence > jitterNextSeq {
// Gap in sequence: skip ahead so we don't
// wait forever for a lost packet.
log.Debug("jitter: seq gap for %s, skipping from %d to %d (buf=%d)",
e.User.Name, jitterNextSeq, jitterBuf[0].Sequence, len(jitterBuf))
jitterNextSeq = jitterBuf[0].Sequence
continue
}
// Sequence == jitterNextSeq but popNext returned nil?
// Shouldn't happen; break to avoid infinite loop.
}
break
}