v1.0 stable — MIT / Apache-2.0

The ultimate
audio router

Rules-driven audio routing for Linux, macOS, and Windows. Sub-100 ms reaction time, Rhai scripting, session snapshots, and full observability. Written in Rust.

Get started View on GitHub →
<100ms
reaction time
3
platforms
0
runtime deps
routing rules
sw — soundworm CLI
$ sw list ┌────┬──────────────────────────────┬────────────┬──────────────────────────┬────────┐ │ ID │ Name │ App │ Media Class │ Kind │ ╞════╪══════════════════════════════╪════════════╪══════════════════════════╪════════╡ │ 12 │ alsa_output.pci-0000_00_1f.3 │ │ Audio/Sink │ Sink │ │ 14 │ alsa_input.usb_blue_yeti │ │ Audio/Source │ Source │ │ 28 │ Spotify │ spotify │ Stream/Output/Audio │ Source │ │ 31 │ ZOOM VoiceEngine │ zoom │ Stream/Input/Audio │ Sink │ │ 35 │ Firefox │ firefox │ Stream/Output/Audio │ Source │ └────┴──────────────────────────────┴────────────┴──────────────────────────┴────────┘ $ sw link Spotify alsa_output.pci-0000_00_1f.3 Linked 'Spotify' → 'alsa_output.pci-0000_00_1f.3' (port 29 → 13) $ sw snapshot save work-from-home Saved snapshot 'work-from-home' $ sw metrics Node 28 latency=4.2ms cpu=0.3% xruns=0 Node 14 latency=2.1ms cpu=0.1% xruns=0

Everything you need.
Nothing you don't.

soundworm is library-first. The daemon and CLI are thin wrappers over a clean Rust API you can embed in anything.

🔀

Rules-driven routing

Declare routing rules in TOML. When a node appears, soundworm matches it against your rules and wires it instantly — no manual intervention needed.

📜

Rhai scripting

When TOML isn't expressive enough, drop into a Rhai script. Full access to node metadata, hot-reload on save, and a 50 ms execution budget to keep things honest.

Sub-100 ms reaction

The PipeWire event loop runs in a dedicated thread. From the moment a node appears on the graph to the moment it is routed: guaranteed under 100 ms.

💾

Session snapshots

Capture your entire routing session with sw snapshot save. Restore it later with a single command — even after a reboot.

📊

Built-in observability

Per-node latency histograms and xrun counters, exposed via sw metrics or an optional Prometheus exporter. Know when something glitches before your ears do.

🦀

Pure Rust, semver-stable

Zero unsafe code in the public API. Semver guarantees on soundworm-core, soundworm-graph, and the IPC protocol from v1.0 onward.

Three platforms.
One API.

The AudioBackend trait abstracts over platform audio systems. Swap backends without touching your rules or scripts.

PlatformBackendEnumerateLink / UnlinkEventsMetrics
Linux PipeWire ✓ stable ✓ stable ✓ stable ✓ stable
macOS CoreAudio ✓ stable ⚠ app-level ✓ stable ~ partial
Windows WASAPI ✓ stable ⚠ session-level ✓ stable ~ partial

Library-first design.

The daemon and CLI are thin wrappers. Every capability is reachable through the library crates — embed soundworm directly into your own application.

CLI (sw)
Daemon (swd)
Your App
│ Unix socket (JSON-RPC) / direct library call │
soundworm-graph
soundworm-policy
soundworm-observability
│ AudioBackend trait │
pipewire-backend
coreaudio-backend
wasapi-backend

Declare once.
Route forever.

Start with TOML rules. Graduate to Rhai when you need conditionals, loops, or external lookups — without restarting the daemon.

TOML rules
Rhai script
Rust API
~/.config/soundworm/rules/default.toml
# Higher priority wins when multiple rules match. [[rules]] name = "spotify-to-speakers" priority = 10 [rules.matches] node_name = "Spotify" [rules.action] Route = { target = "alsa_output.pci-0000_00_1f.3" } [[rules]] name = "zoom-usb-headset" priority = 20 [rules.matches] node_name = "ZOOM VoiceEngine" [rules.action] Route = { target = "alsa_input.usb_blue_yeti" } [[rules]] name = "block-obs-loopback" priority = 99 [rules.matches] node_name = "OBS Audio Monitor" [rules.action] Deny = {}
~/.config/soundworm/scripts/routing.rhai
// Called for every new node. `node_name`, `app_name`, `media_class` are in scope. // Hot-reloads within 1 s of saving. Previous script stays active on parse error. let music_apps = ["Spotify", "vlc", "mpv", "Rhythmbox"]; let comms_apps = ["ZOOM VoiceEngine", "Microsoft Teams", "Signal Desktop"]; if music_apps.contains(node_name) { log_route(node_name, "speakers"); allow() } else if comms_apps.contains(node_name) { log_route(node_name, "usb_headset"); allow() } else if media_class == "Stream/Output/Audio" { // Unknown stream: route to default sink, log it log_route(node_name, "default"); allow() } else { deny() }
src/main.rs
use soundworm_core::backend::AudioBackend; use soundworm_graph::AudioGraph; use soundworm_pipewire::PipeWireBackend; let backend = PipeWireBackend::new()?; // Subscribe to live graph events let rx = backend.subscribe(); let mut graph = AudioGraph::new(); std::thread::spawn(move || { while let Ok(event) = rx.recv() { graph.apply_event(event); } }); // Manual link by node name let nodes = backend.enumerate_nodes().await?; // ... find ports, create link ... backend.create_link(&link).await?;

Up in under a minute.

Install from crates.io or your distro's package manager. The systemd user service starts automatically on login.

Fedora / RPM (COPR)
sudo dnf copr enable grokimagecompression/soundworm sudo dnf install soundworm systemctl --user enable --now soundworm
Arch Linux (AUR)
yay -S soundworm # or paru -S soundworm
macOS (Homebrew)
brew tap grokimagecompression/soundworm brew install soundworm brew services start soundworm
cargo (all platforms)
cargo install soundworm-cli soundworm-daemon # start the daemon swd & # verify sw list
systemd user service (Linux)
mkdir -p ~/.config/systemd/user cp $(cargo locate-project --workspace --message-format plain | xargs dirname)/contrib/systemd/soundworm.service ~/.config/systemd/user/ systemctl --user enable --now soundworm systemctl --user status soundworm

sw — the soundworm CLI

Every command works in-process when the daemon is down. Read-only commands (list, metrics) never need a running daemon.

sw list
List all audio nodes in a formatted table with ID, name, app, media class, and kind.
sw link <src-node> <sink-node>
Create a route between two nodes by name. Finds a matching output/input port pair automatically.
sw unlink <link-id>
Remove a route by its link ID (shown in sw list after linking).
sw snapshot save <name>
Capture the current routing session to $XDG_DATA_HOME/soundworm/snapshots/<name>.json.
sw snapshot load <name>
Restore a previously saved session, diffing against the current graph and applying only what changed.
sw snapshot list
List all saved snapshots by name.
sw metrics
Dump per-node latency histograms and xrun counters as a formatted table. Add --json for machine-readable output.
sw metrics --watch
Stream live metrics updates to the terminal, refreshing every second.
Environment variables
RUST_LOG error | warn | info | debug | trace (default: warn)
XDG_CONFIG_HOME config dir (default: ~/.config)
XDG_DATA_HOME snapshot dir (default: ~/.local/share)
SOUNDWORM_BACKEND pipewire | coreaudio | wasapi (default: auto)