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

  1. Use a wired LAN connection for all machines.
  2. Set the broadcaster’s PipeWire quantum to 128 or 256 frames.
  3. Reduce relay buffer: --buffer-ms 5.
  4. If xruns appear, increase the broadcaster quantum before touching
    broadcast_frames.
  5. Reduce broadcast_frames = 64 in shiloh-mixer.toml for 1.33 ms batching
    (at the cost of 2× more UDP packets — 750 pps).

NOTE: broadcast_frames in shiloh-mixer.toml controls the mixer’s outgoing relay
packet size. It is sent to broadcasters in the ACCEPT_TX response, so all sides
use the same frame count per packet.