Signal Flow
Audio Path End to End
[Broadcaster machine]
App routes audio to PipeWire null-sink
│
▼
shiloh-broadcaster: JACK client "name-send"
PipeWire exposes null-sink monitor ports as:
{sink_name}:monitor_AUX0
{sink_name}:monitor_AUX1 (stereo)
autoconnect thread wires these to:
{sender_name}-send:in_1
{sender_name}-send:in_2
│
▼ JACK RT callback → per-channel rtrb ring (producer side)
│
▼ TX thread reads from ring consumers
encode S16LE, build AUDIO_TX packet
UDP send → mixer :5005
│
▼
[Mixer server — shiloh-mixer]
broadcast thread receives AUDIO_TX
decode S16LE → f32
push into per-slot ingest rings (rtrb, broadcast_frames × 64 capacity)
│
▼ JACK RT callback (runs on JACK RT thread)
pop from ingest rings for each channel with ingest_slot set
add_channel() per channel:
main_l += ch_L × main_gain × (1 − muted)
main_r += ch_R × main_gain × (1 − muted)
monitor_l += ch_L × monitor_gain × (1 − monitor_muted)
monitor_r += ch_R × monitor_gain × (1 − monitor_muted)
cue_l += ch_L × cue_gain × (1 − cue_muted)
cue_r += ch_R × cue_gain × (1 − cue_muted)
master_gain applied; output clipped to [-1, 1]
relay tap: main/monitor/cue × relay_gain × relay_on
→ push into relay SPSC ring (broadcast thread consumes)
│
▼ JACK output ports wired by PipeWire autoconnect
system:playback (USB DAC) ← main bus
Scarlett headphone out ← monitor bus
│
▼ broadcast thread
per-client relay send:
each registered client has an assigned feed (main/monitor/cue)
encode f32 → S16LE, build AUDIO packet
UDP send to all registered relay clients (shiloh-relay, shiloh-web-relay)
│
▼
[shiloh-relay]
rx thread receives AUDIO packets
decode S16LE → f32
push into rtrb ring
cpal callback pops from ring → ALSA output
│
▼
[Local audio device on listener machine]
[shiloh-web-relay]
relay_sub thread receives AUDIO packets
mpsc channel → encode task
Opus encode (960 frames = 20 ms, 48 kHz, stereo, 128 kbps VBR)
webrtc-rs writes RTP to all connected PeerConnections
│
▼
[Browser WebRTC session]
Packet Format
The UDP audio packet format differs slightly between directions (ingest and relay output):
AUDIO relay packet (mixer → relay clients):
| Field | Offset | Size | Value |
|---|---|---|---|
| type tag | 0 | 1 byte | 0x04 (T_AUDIO) |
| session_id | 1 | 4 bytes | u32 LE, server-assigned at REGISTER |
| seq | 5 | 4 bytes | u32 LE, monotonic, wraps at 2^32 |
| samples | 9 | variable | interleaved S16LE, frames × channels × 2 bytes |
AUDIO_TX ingest packet (broadcaster → mixer):
| Field | Offset | Size | Value |
|---|---|---|---|
| type tag | 0 | 1 byte | 0x13 (T_AUDIO_TX) |
| session_id | 1 | 4 bytes | u32 LE, server-assigned at ACCEPT_TX |
| seq | 5 | 4 bytes | u32 LE, monotonic, wraps at 2^32 |
| channels | 9 | 1 byte | Number of interleaved channels (e.g. 2) |
| samples | 10 | variable | interleaved S16LE, frames × channels × 2 bytes |
Default parameters: 48 kHz, 128 frames/packet, stereo (2 ch)
- Payload: 128 × 2 × 2 = 512 bytes
- Total packet: 521 bytes
- Packet rate: 48000 / 128 = 375 pps
- Bandwidth per client: ~1.6 Mbit/s
- Duration per packet: ~2.67 ms
Lost packets become silence on the receiver. There is no retransmit. At 2.67 ms per
packet, individual losses are inaudible; a burst of 3–4 consecutive losses (~10 ms) may
produce a faint click.
Output Buses
Three program buses are summed by the JACK RT callback:
| Bus | Ports | Muted behavior | Typical use |
|---|---|---|---|
| main | out_1, out_2 |
Per-channel mute | Primary program output to PA/recording |
| monitor | monitor_out_1, monitor_out_2 |
Independently mutable per bus (default: unmuted) | Headphone cue |
| cue | cue_out_1, cue_out_2 |
Independently mutable per bus (default: unmuted) | Per-channel cue sends |
Each bus has a corresponding relay feed (main_relay, monitor_relay, cue_relay) with
its own gain and on/off gate. Relay clients each subscribe to exactly one feed; the
assignment persists by client name across restarts.
Thread Model
JACK RT thread broadcast thread control thread
process_callback() UdpSocket recv loop UdpSocket recv :19997
├─ pop ingest rings ├─ handle REGISTER_TX ├─ JSON commands from mixer_web
├─ add_channel() × N ├─ handle AUDIO_TX │ (fader, mute, scene ops)
├─ relay tap │ push to ingest rings └─ updates ArcSwap<Scene> target
│ push relay ring ├─ fan-out AUDIO to clients → RT thread reads via Ramp::tick() on next callback
└─ write JACK out ports └─ write sidecar JSON files
(sessions, diag, relay-assignments,
session-delays)
meters writer thread
polls RT thread meters
writes meters.json (~10 Hz)
broadcaster TX thread broadcaster heartbeat thread
wait ring consumers ≥ frames send PING every 1 s
build AUDIO_TX packet to keep mixer session alive
UDP send → :5005
The RT thread and the broadcast thread communicate exclusively through rtrb SPSC rings:
no mutexes on the hot path. Control commands use a separate rtrb queue; whole-scene
snapshots use ArcSwap for atomic replacement without blocking the RT thread.