nx_jitterbuf.nx source
↩ module page · 35 lines · 2095 B
1// nx_jitterbuf.nx -- sovereign ADAPTIVE PLAYOUT DELAY (jitter buffer) sizing for the snapshot
2// interpolator (nx_interp). The interp playout delay is the latency/smoothness dial: too small ->
3// the buffer starves (peer freezes/extrapolates) under jitter; too large -> needless lag. This sizes
4// it to MEASURED jitter (RFC 3550 interarrival-jitter smoothing) and GROWS FAST / SHRINKS SLOW so a
5// jitter spike is covered immediately but latency creeps back down as the link calms. 100% nx, no
6// syscalls -> wasm-friendly. The host only timestamps arrivals; all control logic is here.
7//
8// State jb[*i64]: [0]=last_recv_ms [1]=jitter_acc(~16x mean|D|) [2]=send_interval [3]=playout_delay
9// [4]=min_delay [5]=max_delay [6]=count
10// license_tier: ORIGINAL
11
12func jb_init(jb: *i64, send_interval: i64, min_delay: i64, max_delay: i64) -> i64 {
13 jb[0]=0; jb[1]=0; jb[2]=send_interval; jb[3]=min_delay; jb[4]=min_delay; jb[5]=max_delay; jb[6]=0
14 return 0
15}
16func jb_jitter(jb: *i64) -> i64 { return jb[1] / 16 } // smoothed mean deviation of interarrival
17func jb_playout(jb: *i64) -> i64 { return jb[3] }
18
19// call on each snapshot arrival (recv wall-clock ms). Updates the jitter estimate + adapts the
20// playout delay. Returns the current playout delay (what the interp render-time should lag by).
21func jb_on_recv(jb: *i64, recv_ms: i64) -> i64 {
22 if jb[6] > 0 {
23 var d: i64 = (recv_ms - jb[0]) - jb[2] // D = actual gap - expected gap
24 if d < 0 { d = 0 - d } // |D|
25 jb[1] = jb[1] - (jb[1] / 16) + d // exp-smoothed, scaled x16 (keeps integer resolution)
26 }
27 jb[0] = recv_ms
28 jb[6] = jb[6] + 1
29 var target: i64 = jb[2] + (jb[1] / 4) // send_interval + 4*jitter cushion
30 if target < jb[4] { target = jb[4] }
31 if target > jb[5] { target = jb[5] }
32 if target > jb[3] { jb[3] = target } // GROW FAST (cover the spike now)
33 else { jb[3] = jb[3] - ((jb[3] - target) / 8) } // SHRINK SLOW (latency creeps back down)
34 return jb[3]
35}