linux-sonar.
SteelSeries Sonar doesn't exist on Linux. So I built it.
Five virtual audio channels, per-app routing enforced by a daemon, a ChatMix slider with hardware wheel support, and a full mic effects chain (RNNoise, gate, EQ, compressor, limiter) running as an isolated PipeWire filter-chain. GTK4 GUI, waybar integration, no proprietary software anywhere in the path.
CHANNEL.TOPOLOGY
LAYER 01 / APPS
Game · Chat · Media · Aux · Mic
Sources resolved by per-app rules
LAYER 02 / VIRTUAL SINKS
5× filter-chain modules
target.object pinned to headset
LAYER 03 / DAEMON + GUI
Routing daemon · ChatMix · GTK4
WirePlumber persists across reboot
LAYER 04 / OUTPUT
Headset · Mic out
Any PipeWire stereo sink works
Five sinks. One headset.
Each channel is its own PipeWire virtual sink, pinned to the headset via target.object. Apps are routed in by name; the daemon enforces routing every 1.5s so launches, restarts, and streaming sessions don't leak across channels.
| Channel | What lands here |
|---|---|
Game | Default destination for game processes. Routed by daemon polling against per-app rules so launches and respawns don't escape the channel. |
Chat | Voice clients (Discord, Equibop, TeamSpeak) routed here. ChatMix slider balances this against the Game channel against the headset. |
Media | Browsers, music players, video. Anything that's not a game and not a voice client lands here by default. |
Aux | Free slot — bind it to whatever you want. Useful for OBS monitoring, secondary mixes, or apps you want isolated from the rest. |
Mic | Output of the mic effects chain. RNNoise → gate → 8-band EQ → compressor → limiter, all running as a PipeWire filter-chain subprocess. |
What it actually does
Sonar feature parity, no proprietary software, no kernel modules, stock PipeWire on Arch.
Per-app routing
Five virtual sinks pinned to your headset via target.object. A small daemon polls wpctl/pactl every 1.5s and enforces per-app routing rules, so apps stay where they belong even after relaunches.
- libpipewire-module-filter-chain virtual sinks, one per channel
- WirePlumber persists app→channel mappings across reboots
- Daemon catches sink-input regressions inside ~1.5s
ChatMix
Software slider rebalances Game against Chat in real time. Hardware ChatMix wheel works too — read straight off the device over USB-HID, no SteelSeries software in the loop.
- Slider driven from the GTK4 GUI or your waybar module
- Hardware wheel via USB-HID — direct read from the headset
- Game and Chat volumes scale inversely from a single input
Mic effects chain
RNNoise, noise gate, 8-band EQ, compressor, and limiter, in that order. Runs as an isolated pipewire filter-chain subprocess so a bad config can't take the whole graph with it.
- Static-stereo capture.props avoids RnNoiseStereo SEGV on mic swap
- Service-restart swap pattern — no live-relink edge cases
- Each stage tuneable independently in the GUI
GTK4 GUI + waybar
libadwaita panel for live tuning of every channel and the mic chain. Optional waybar module exposes ChatMix and per-channel volumes on the bar — no GUI needed for hot-path adjustments.
- Native GTK4 / libadwaita — fits Hyprland and other Wayland setups
- Waybar module ships per-channel volumes + ChatMix
- GUI stays optional; daemon and CLI work standalone
Five stages, one filter-chain subprocess
The mic effects pipeline runs as an isolated libpipewire-module-filter-chain subprocess. Each stage is independently tuneable. Live re-link on mono ↔ stereo audioconvert renegotiation used to SEGV the RnNoise stage — fixed by static-stereo capture props and a service-restart swap pattern instead of in-place relinks.
ML-based noise suppression — strips fan, keyboard, and ambient noise
Closes when below threshold so room tone never makes it onto the wire
Per-band shelf and peak filters — clean up the mic curve to taste
Even out levels between quiet and loud delivery without losing dynamics
Hard ceiling so the chain can't overshoot and clip downstream
How it's built
Daemon, GUI, and tooling
filter-chain modules for the five virtual sinks and the mic chain
Persists per-app routing decisions across reboots
Native Wayland-friendly GUI
ML-based noise suppression as a PipeWire filter
Direct read of hardware ChatMix wheel
Routing daemon
A small Python daemon polls wpctl and pactl on a ~1.5s cadence. When a sink-input doesn't match its expected channel (because the app just launched, restarted, or spawned a child stream), the daemon moves it. WirePlumber persists those moves so they survive reboots without rerunning setup.
Compatibility
Tested on Arch with stock PipeWire and WirePlumber. Works with any stereo output sink — analog headphone jack, USB headset, HDMI audio, Bluetooth A2DP. EasyEffects' output pipeline must be disabled for per-channel routing to take effect (its global capture sits in front of the per-app sinks).
Getting it running
Install dependencies
PipeWire, WirePlumber, GTK4, libadwaita, and Python 3.11+. On Arch: pacman -S pipewire wireplumber gtk4 libadwaita python.
Clone and run setup
Clone the repo, run the install script. It drops the filter-chain configs into ~/.config/pipewire/pipewire.conf.d/ and seeds the routing daemon's app→channel rules.
Disable EasyEffects output
If you run EasyEffects, turn off its output pipeline — it captures everything before the per-channel sinks can route it. The mic side is fine; only the output stage conflicts.
Launch the GUI
Open the GTK4 panel to verify all five sinks show up and to tune the mic chain. Add the waybar module if you want ChatMix and per-channel volumes on your bar.
Get linux-sonar
FOSS under GPL-3.0. Clone, hack, and route. Issues and PRs welcome.