Jitter buffer v2, per-minute rx logging, websocket keepalive
The cohost sounded jittery on the 2026-11-03 show. v1 skipped ahead on an underrun and kept playing from an empty buffer, so after the first hiccup every later wobble was another glitch. It also never corrected a slow guest clock: ~12 glitches/min in simulation at 30 ppm slow. - Underrun: hold the sequence position and refill to a larger target - Drift correction both ways (drop/insert a frame in quiet moments), quiet judged relative to the guest's own noise floor - 80 ms start, 300 ms max, relaxes after 2 calm minutes - Log 'rx 1m:' stats, the selected route, and call totals - Ping the guest websocket every 25 s (the proxy closed it every 90 s) - Simulation kept in test/jb_sim_test.go.txt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b0e3b805b1
commit
de7ed62d96
5 files changed
+346
-64
No files matched your search
@@ -0,0 +1,86 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/pion/rtp"
|
||||
"gopkg.in/hraban/opus.v2"
|
||||
)
|
||||
|
||||
// Simulate: sender emits a packet every 20ms*(1+ppm) of its clock; network adds jitter;
|
||||
// playout pulls one frame every 20ms of MOTU clock. Count audible damage over 3 hours.
|
||||
func sim(t *testing.T, name string, noiseFloor float64, ppm float64, jitterMs func(r *rand.Rand) float64) {
|
||||
enc, _ := opus.NewEncoder(rate, channels, opus.AppAudio)
|
||||
nr := rand.New(rand.NewSource(7))
|
||||
mk := func(i int, talk bool) []byte {
|
||||
pcm := make([]int16, frameSamps*channels)
|
||||
for k := range pcm {
|
||||
n := nr.NormFloat64() * noiseFloor * 32768
|
||||
v := n
|
||||
if talk { v += 8000 * math.Sin(float64(i*frameSamps+k/2)*2*math.Pi*220/rate) * (0.5 + 0.5*math.Sin(float64(i)*0.9)) }
|
||||
pcm[k] = int16(v)
|
||||
}
|
||||
buf := make([]byte, 4000); n, _ := enc.Encode(pcm, buf); return append([]byte(nil), buf[:n]...)
|
||||
}
|
||||
var talkP, quietP [][]byte
|
||||
for i := 0; i < 50; i++ { talkP = append(talkP, mk(i, true)); quietP = append(quietP, mk(i, false)) }
|
||||
payloadFor := func(seq int) []byte { if seq%175 < 125 { return talkP[seq%50] }; return quietP[seq%50] }
|
||||
dec, _ := opus.NewDecoder(rate, channels)
|
||||
jb := newJitterBuffer(dec)
|
||||
r := rand.New(rand.NewSource(1))
|
||||
const dur = 3 * 3600 * 1000.0
|
||||
type ev struct{ at float64; seq uint16 }
|
||||
var arr []ev
|
||||
period := 20.0 * (1 + ppm*1e-6)
|
||||
for i := 0; float64(i)*period < dur; i++ {
|
||||
arr = append(arr, ev{float64(i)*period + 50 + jitterMs(r), uint16(i)})
|
||||
}
|
||||
sort.Slice(arr, func(a, b int) bool { return arr[a].at < arr[b].at })
|
||||
k := 0
|
||||
depthSum, frames, nearEmpty := 0, 0, 0
|
||||
for tms := 0.0; tms < dur; tms += 20 {
|
||||
for k < len(arr) && arr[k].at <= tms { jb.push(&rtp.Packet{Header: rtp.Header{SequenceNumber: arr[k].seq}, Payload: payloadFor(int(arr[k].seq) + 65536*0)}); k++ }
|
||||
jb.next()
|
||||
d := len(jb.pkts); depthSum += d; frames++
|
||||
if d == 0 { nearEmpty++ }
|
||||
}
|
||||
s := jb.stats()
|
||||
bad := s["plc"].(int) + s["fec"].(int) + s["dropped"].(int)
|
||||
fmt.Printf("%-34s glitches(PLC+FEC)=%5d (~%.1f/min) underruns=%4d late-discarded=%5d drops=%4d inserted=%4d avg depth=%.0fms empty %.0f%% of time final target=%v\n",
|
||||
name, bad, float64(bad)/180, s["underruns"], s["late"], s["dropped"], s["inserted"], float64(depthSum)/float64(frames)*20, 100*float64(nearEmpty)/float64(frames), s["targetMs"])
|
||||
}
|
||||
|
||||
func TestSim(t *testing.T) {
|
||||
for _, nf := range []float64{0.0005, 0.006} {
|
||||
fmt.Printf("--- pauses at %.0f dBFS noise floor\n", 20*math.Log10(nf))
|
||||
gauss := func(sd float64) func(*rand.Rand) float64 { return func(r *rand.Rand) float64 { return math.Abs(r.NormFloat64() * sd) } }
|
||||
spiky := func(r *rand.Rand) float64 { // typical home Wi-Fi: small jitter + occasional 80-200ms stalls
|
||||
j := math.Abs(r.NormFloat64() * 4)
|
||||
if r.Float64() < 0.002 { j += 80 + r.Float64()*120 }
|
||||
return j
|
||||
}
|
||||
sim(t, "clean net, no drift", nf, 0, gauss(0.5))
|
||||
sim(t, "5ms jitter, no drift", nf, 0, gauss(5))
|
||||
sim(t, "5ms jitter, guest clock 30ppm FAST", nf, -30, gauss(5))
|
||||
sim(t, "5ms jitter, guest clock 30ppm SLOW", nf, 30, gauss(5))
|
||||
sim(t, "Wi-Fi-ish spikes, no drift", nf, 0, spiky)
|
||||
sim(t, "Wi-Fi-ish spikes, guest 30ppm SLOW", nf, 30, spiky)
|
||||
// correlated stall: every packet in a 150 ms window is held until the window ends
|
||||
burst := func() func(*rand.Rand) float64 {
|
||||
i, until := 0, -1.0
|
||||
return func(r *rand.Rand) float64 {
|
||||
now := float64(i) * 20; i++
|
||||
if now > until && r.Float64() < 0.0005 { until = now + 150 }
|
||||
j := math.Abs(r.NormFloat64() * 3)
|
||||
if now <= until { j += until - now }
|
||||
return j
|
||||
}
|
||||
}
|
||||
sim(t, "burst stalls 150ms, no drift", nf, 0, burst())
|
||||
sim(t, "burst stalls 150ms, guest SLOW", nf, 30, burst())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user