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>
87 lines
3.6 KiB
Plaintext
87 lines
3.6 KiB
Plaintext
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())
|
|
}
|
|
}
|