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, 111) 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()) } }