Files
guestlink/test/jb_sim_test.go.txt
T
namandClaude Opus 5.5 f8d0e6fc36 Skip RTP padding packets instead of concealing them
Padding/probe packets use a sequence number but carry no audio. They were
treated as lost, so each one played an invented 20 ms frame (a glitch) and
pushed the buffer a frame deeper for drift correction to remove again.
Signature in the first live test: lost ~= drift-drops (74 vs 72 per minute).
They are now skipped and counted as 'padding'.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 11:22:00 -06:00

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