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.