Latency Characteristics
LAN Path
[Broadcaster machine]
PipeWire null-sink captures audio at PW quantum
↓ TX thread wakes when ring has ≥ 128 frames (2.67 ms)
build AUDIO_TX packet, UDP send
↓ LAN RTT < 1 ms
[Mixer server] broadcast thread receives AUDIO_TX
↓ push to ingest ring (128 × 64 = 170 ms headroom)
JACK RT callback pops from ingest ring every 128 frames (2.67 ms)
↓ mix math, relay tap
push to relay SPSC ring
↓ broadcast thread encodes and sends UDP AUDIO every 128 frames
↓ LAN RTT < 1 ms
[Relay machine] rx thread receives AUDIO
↓ push to cpal ring (10 ms buffer default)
cpal callback writes to ALSA output
Estimated LAN Latency
The dominant factor is the broadcaster’s PipeWire quantum — the mixer and relay
are fixed at known values. Total end-to-end latency at 48 kHz:
| Broadcaster quantum | Broadcaster PW | TX batch (128 fr) | Mixer (128 fr) | Relay batch (128 fr) | Relay buffer | Total |
|---|---|---|---|---|---|---|
| 128 frames | 2.7 ms | 2.7 ms | ~4 ms | 2.7 ms | 10 ms | ~23 ms |
| 256 frames | 5.3 ms | 2.7 ms | ~4 ms | 2.7 ms | 10 ms | ~26 ms |
| 512 frames | 10.7 ms | 2.7 ms | ~4 ms | 2.7 ms | 10 ms | ~31 ms |
| 1024 frames | 21.3 ms | 2.7 ms | ~4 ms | 2.7 ms | 10 ms | ~42 ms |
Mixer column includes one JACK period (2.67 ms) + ingest ring steady-state depth (~1 period).
Network RTT is negligible on wired LAN (< 1 ms total).
Browser/WebRTC Path
[Mixer] relay fan-out → shiloh-web-relay
↓ Opus encode (960 frames = 20 ms chunks)
↓ webrtc-rs → RTP → ICE/DTLS/SRTP
↓ network (LAN or WAN)
[Browser] WebRTC audio decoding + jitter buffer
Estimated total: 100–500 ms
WebRTC adds:
- Opus encoding latency: ~20 ms (one 960-frame chunk)
- webrtc-rs RTP packetization: ~5–10 ms
- Browser jitter buffer: typically 60–200 ms (browser-controlled)
- Network RTT: 0–100 ms depending on path
Browser latency is not tunable from the application side — the browser’s jitter buffer is
controlled by the WebRTC stack.
Factors That Affect LAN Latency
Broadcaster PipeWire quantum
The broadcaster machine’s PipeWire quantum is the single biggest knob. On the
broadcaster machine, run these diagnostics first:
# Check current quantum
pw-metadata -n settings | grep clock.force-quantum
# Check actual period being used (look for alsa.pcm devices)
pw-top
If clock.force-quantum is unset, PipeWire uses its default (typically 1024 frames
on most distros). Set it with:
# Temporary (lost on reboot):
pw-metadata -n settings 0 clock.force-quantum 128
# Permanent: create a config snippet
mkdir -p ~/.config/pipewire/pipewire.conf.d
cat > ~/.config/pipewire/pipewire.conf.d/quantum.conf << 'EOF'
context.properties = {
default.clock.force-quantum = 128
}
EOF
systemctl --user restart pipewire pipewire-pulse
After changing, verify the quantum took effect by running pw-top on the
broadcaster machine while the broadcaster is streaming — the period column
should show your configured value.
| Period size | Latency per hop | xrun risk |
|---|---|---|
| 128 frames | 2.67 ms | Higher — needs RT kernel or preempt=full |
| 256 frames | 5.33 ms | Moderate — works on most tuned systems |
| 512 frames | 10.67 ms | Low — safe default for standard kernels |
| 1024 frames | 21.33 ms | Minimal — but latency suffers |
If xruns appear after reducing the quantum:
# Check xrun count on the broadcaster
cat /proc/asound/card*/pcm*p/sub0/hw_params 2>/dev/null
# Or watch live in pw-top (look for ERR column)
pw-top
On a well-tuned RT kernel (preempt=full + threadirqs), 128 frames is
achievable. On a standard kernel, 256–512 is safer.
broadcast_frames (packet size)
The mixer’s broadcast_frames controls how many samples are packed into each UDP packet.
The current config uses 128 frames (2.67 ms at 48 kHz). Each packet carries 128 samples
per channel, and the TX loops on broadcaster, mixer, and relay all batch to this size.
Smaller values reduce batching latency but increase packet rate:
- 64 frames = 1.33 ms → 750 pps
- 128 frames = 2.67 ms → 375 pps
- 160 frames = 3.33 ms → 300 pps
Relay buffer_ms
shiloh-relay --buffer-ms controls the local cpal ring depth on the relay machine.
Default: 10 ms.
# Lower latency, higher underrun risk:
shiloh-relay connect --server ... --buffer-ms 5
# More robust on Wi-Fi:
shiloh-relay connect --server ... --buffer-ms 20
Ingest ring sizing
The mixer’s per-slot ingest ring is broadcast_frames × 64 = 8192 frames = ~170 ms
at 48 kHz. This is headroom for network jitter absorption, not added delay. The RT thread
pops on every JACK callback; it only incurs delay when the ring is genuinely behind. At
steady state, ring depth is roughly one JACK period.
Network RTT
On a wired LAN, RTT is typically < 1 ms and has negligible effect. On Wi-Fi, jitter can
be 1–10 ms and may contribute to occasional ingest ring underruns (silent frames in the mix).
How to Measure
Round-trip latency (broadcaster → relay)
On the broadcaster machine, play a click track to the null-sink. On the relay machine,
record the output with a loopback. Measure the offset.
# Record 5 s of relay output on the Pi
arecord -D hw:0,0 -r 48000 -c 2 -f S16_LE -d 5 relay-capture.wav
# Compare to the click source using audacity or sox
Ingest ring depth
Run shiloh-broadcaster connect --verbose. The stats line shows:
[stat] 375 pps underruns=0 ring_full=0
underruns > 0 means the TX thread had to emit silence. Check JACK xrun count on the
broadcaster side.
Mixer ingest drops
Check shiloh-mixer logs for ingest drops lines, or read the /diag endpoint in
mixer_web which shows per-slot drop counters.
Tuning for Minimum Latency
- Use a wired LAN connection for all machines.
- Set the broadcaster’s PipeWire quantum to 128 or 256 frames.
- Reduce relay buffer:
--buffer-ms 5. - If xruns appear, increase the broadcaster quantum before touching
broadcast_frames. - Reduce
broadcast_frames = 64inshiloh-mixer.tomlfor 1.33 ms batching
(at the cost of 2× more UDP packets — 750 pps).
NOTE:
broadcast_framesinshiloh-mixer.tomlcontrols the mixer’s outgoing relay
packet size. It is sent to broadcasters in theACCEPT_TXresponse, so all sides
use the same frame count per packet.