Installing the Mixer Server
shiloh-mixer is the hub. It registers as a JACK client, mixes all audio channels, and fans the result out over UDP to every connected relay and broadcaster.
Prerequisites
- Linux (tested on Debian/Ubuntu)
- JACK audio — either classic
jackd2or thepipewire-jackdrop-in:# Classic JACK sudo apt install jackd2 jack-tools # Or: PipeWire's JACK compatibility layer (recommended on modern distros) sudo apt install pipewire-jack - Rust toolchain via rustup:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source "$HOME/.cargo/env" - Real-time scheduling permissions — add yourself to the
audiogroup (or configure/etc/security/limits.d/):sudo usermod -aG audio "$USER" # Log out and back in for the group to take effect
1. Clone the repository
git clone https://git.shilohbv.com/shiloh/broadcaster ~/shiloh-broadcaster
cd ~/shiloh-broadcaster
2. Build
cargo build --release -p shiloh-mixer
The binary lands at target/release/shiloh-mixer.
NOTE: The
[profile.release]in the workspaceCargo.tomlenables LTO and strips the binary. The first build will be slow (several minutes). Subsequent incremental builds are fast.
3. Create the working directory
mkdir -p ~/mixer
The systemd unit sets WorkingDirectory=%h/mixer, so the mixer must be able to find its config and state files there. Copy or symlink the binary:
cp target/release/shiloh-mixer ~/mixer/shiloh-mixer
# or use the release binary name the unit expects:
cp target/release/shiloh-mixer ~/mixer/shiloh-mixer-linux-x86_64
4. Write a minimal config file
Create ~/mixer/shiloh-mixer.toml. Below is a minimal working example with two stereo ingest channels (one studio laptop, one guest):
## shiloh-mixer.toml — minimal two-channel example
jack_name = "shiloh-mixer"
control_port = 19997 # mixer_web talks here
broadcast_port = 5005 # relay + broadcaster clients connect here
broadcast_frames = 128 # 128 frames @ 48 kHz = 2.67 ms packets
ramp_ms = 150 # fader ramp time for scene transitions
meters_file = "/home/shiloh/mixer-state/meters.json"
gains_file = "/home/shiloh/mixer-state/gains.json"
scenes_file = "/home/shiloh/mixer-state/scenes.json"
relay_assignments_file = "/home/shiloh/mixer-state/relay-assignments.json"
# ── channels ─────────────────────────────────────────────────────────
# ingest_slot links this channel to the broadcaster ingest bank.
# Stereo channels consume two consecutive slots.
[[channels]]
id = "studio"
label = "Studio"
kind = "stereo"
main_gain = 1.0
ingest_slot = 0
[[channels]]
id = "guest"
label = "Guest"
kind = "stereo"
main_gain = 1.0
ingest_slot = 2
# ── output buses ─────────────────────────────────────────────────────
[buses]
main = ["out_1", "out_2"]
main_gain = 1.0
monitor = ["monitor_out_1", "monitor_out_2"]
monitor_gain = 1.0
cue = ["cue_out_1", "cue_out_2"]
cue_gain = 1.0
main_relay = ["main_relay_line_1", "main_relay_line_2"]
main_relay_gain = 1.0
main_relay_on = true
monitor_relay = ["monitor_relay_line_1", "monitor_relay_line_2"]
monitor_relay_gain = 1.0
monitor_relay_on = true
cue_relay = ["cue_relay_line_1", "cue_relay_line_2"]
cue_relay_gain = 1.0
cue_relay_on = true
# ── ingest (broadcaster senders) ─────────────────────────────────────
[ingest]
slot_count = 4
[[ingest.sender]]
name = "studio"
channels = 2
start_slot = 0
[[ingest.sender]]
name = "guest"
channels = 2
start_slot = 2
Create the state directory referenced above:
mkdir -p ~/mixer-state
NOTE: The
namein[[ingest.sender]]must exactly match the--nameflag passed toshiloh-broadcasteron the sender machine. If a sender connects with an unrecognised name the mixer will reject it.
5. First run
Make sure JACK is running first:
# With pipewire-jack: JACK is already running as part of PipeWire
pw-jack jackd -d dummy & # only needed if PipeWire is not started
# Then start the mixer:
RUST_LOG=info ~/mixer/shiloh-mixer --config ~/mixer/shiloh-mixer.toml
Look for these lines in the output to confirm a healthy start:
INFO shiloh_mixer: JACK client "shiloh-mixer" registered
INFO shiloh_mixer: broadcast server listening on 0.0.0.0:5005
INFO shiloh_mixer: control plane listening on 127.0.0.1:19997
Verify the CLI help is accessible:
~/mixer/shiloh-mixer --help
Stop with Ctrl-C. The mixer sends a graceful BYE to all connected clients.
6. systemd user unit
The repo ships a ready-made user service at server/systemd/shiloh-mixer.service. Install it:
mkdir -p ~/.config/systemd/user
cp ~/shiloh-broadcaster/server/systemd/shiloh-mixer.service \
~/.config/systemd/user/shiloh-mixer.service
The unit file (shown below for reference) expects the binary at ~/mixer/shiloh-mixer-linux-x86_64 and the config at ~/mixer/shiloh-mixer.toml:
[Unit]
Description=shiloh-mixer — Rust JACK mix bus + UDP broadcaster relay
After=pipewire.service wireplumber.service
Wants=pipewire.service wireplumber.service
[Service]
Type=simple
WorkingDirectory=%h/mixer
# pw-jack wraps the mixer so it uses PipeWire's JACK shim instead of native libjack.
# This makes PipeWire clients (Firefox, Desktop Audio) visible as JACK ports.
ExecStart=/usr/bin/pw-jack %h/mixer/shiloh-mixer-linux-x86_64 --config %h/mixer/shiloh-mixer.toml
Restart=on-failure
RestartSec=3
LimitRTPRIO=95
LimitMEMLOCK=infinity
Environment=XDG_RUNTIME_DIR=/run/user/1000
Environment=RUST_LOG=info
Environment=RUST_BACKTRACE=1
[Install]
WantedBy=default.target
NOTE: Replace
1000inXDG_RUNTIME_DIRwith your actual UID (id -u). PipeWire and JACK use this path to find the audio session socket.
NOTE: The
pw-jackwrapper is required on modern PipeWire systems. Without it, the mixer uses nativelibjackand cannot see PipeWire-managed audio sources (Firefox, Desktop Audio, etc.).
Enable and start:
systemctl --user daemon-reload
systemctl --user enable shiloh-mixer.service
systemctl --user start shiloh-mixer.service
systemctl --user status shiloh-mixer.service
To follow live logs:
journalctl --user -u shiloh-mixer.service -f
Troubleshooting
| Symptom | Likely cause |
|---|---|
JACK server not running |
PipeWire/jackd not started yet; check pw-jack jack_lsp |
broadcast server bind failed |
Port 5005 already in use; check `ss -ulnp |
rejected sender "studio": not in allow-list |
Name mismatch between [[ingest.sender]] and --name on the broadcaster |
| High CPU after many reconnects | Increase broadcast_frames to reduce packet rate |