Full audio routing for a streaming/gaming PC: processed mic via EasyEffects, software soundboard mixed into Discord, and raw mic monitor out to a second machine — all running simultaneously without error loops or exclusive-access conflicts.
Tested on: Arch Linux (CachyOS), PipeWire 1.x, WirePlumber 0.5.x, EasyEffects 7.x
Elgato Wave XLR (Shure SM7B)
└─► EasyEffects (deesser → compressor → rnnoise)
└─► easyeffects_source (virtual mic)
├─► Discord mic / CS2 mic input
└─► soundboard_mic loopback ─────────────────┐
▼
VLC / StreamController (soundboard clips) ──────► soundboard_sink
│
soundboard_sink.monitor
│
SoundboardMonitor (remap-source)
│
Discord mic input
All app audio ──► Sound BlasterX AE-5 (system default output)
Elgato Wave XLR (raw, pre-EasyEffects)
└─► mic_monitor loopback ──► Elgato headphone jack ──► second PC
| Device | PipeWire node | Role |
|---|---|---|
| Sound BlasterX AE-5 Plus (PCIe) | alsa_output.pci-0000_05_00.0.analog-stereo |
System default output |
| Elgato Wave XLR — mic in | ...Wave_XLR...pro-input-0 |
Mic input (Shure SM7B) |
| Elgato Wave XLR — headphone out | ...Wave_XLR...pro-output-0 |
Raw mic monitor to second PC |
| Motherboard analog | alsa_output.pci-0000_00_1f.3.analog-stereo |
Discord chat output |
Full Elgato node names used verbatim in config files:
alsa_input.usb-Elgato_Systems_Elgato_Wave_XLR_A01DA349211E0W-00.pro-input-0
alsa_output.usb-Elgato_Systems_Elgato_Wave_XLR_A01DA349211E0W-00.pro-output-0
Find your own device node names:
pactl list sources short # mic inputs
pactl list sinks short # audio outputs- PipeWire + WirePlumber running (not PulseAudio)
pipewire-pulseinstalled (PulseAudio compatibility layer)- EasyEffects installed and configured with your desired mic plugin chain
pw-linkandpactlavailable (inpipewireandlibpulsepackages)
Verify PipeWire is active:
pactl info | grep "Server Name"
# Expected: Server Name: PulseAudio (on PipeWire ...)WirePlumber persists the default sink/source and device profiles across reboots via these state files. Set them while WirePlumber is running — it reads them on startup.
~/.local/state/wireplumber/default-nodes
[default-nodes]
default.configured.audio.sink=alsa_output.pci-0000_05_00.0.analog-stereo
default.configured.audio.source=alsa_input.usb-Elgato_Systems_Elgato_Wave_XLR_A01DA349211E0W-00.pro-input-0~/.local/state/wireplumber/default-profile
[default-profile]
alsa_card.pci-0000_05_00.0=output:analog-stereo
alsa_card.pci-0000_00_1f.3=output:analog-stereo
alsa_card.usb-Elgato_Systems_Elgato_Wave_XLR_A01DA349211E0W-00=pro-audioThe pro-audio card profile on the Elgato is required. Without it, WirePlumber may leave the
device on the off profile after reboot — pro-input-0 and pro-output-0 will not appear as
separate nodes and the entire setup will be broken.
The system default source must always be the raw Elgato mic node, never
easyeffects_sourceor any virtual source. EasyEffects reads the system default source at startup and writes it back toeasyeffectsrc. If it finds a virtual source as the default, it tries to capture its own output — a silent feedback loop that produces no signal and corrupts the config file.
Only list node names that currently exist in
default-nodes. Stale entries for nodes that no longer exist cause WirePlumber to misbehave silently with no logged errors. Verify active names withpactl list sinks shortandpactl list sources shortany time you change hardware.
~/.config/easyeffects/db/easyeffectsrc
[StreamInputs]
blocklist=mic_monitor_capture,soundboard_mic_capture
inputDevice=alsa_input.usb-Elgato_Systems_Elgato_Wave_XLR_A01DA349211E0W-00.pro-input-0
plugins=deesser#0,compressor#0,rnnoise#0
showBlocklistedApps=true
[StreamOutputs]
blocklist=mic_monitor_playback,soundboard_mic_playback
outputDevice=alsa_output.pci-0000_05_00.0.analog-stereo
showBlocklistedApps=trueThe blocklists exclude the loopback stream nodes created in Steps 4–5 from EasyEffects processing. Without them, EasyEffects intercepts the loopback streams as additional mic inputs, applies plugin processing to them, and disrupts routing.
The soundboard uses a null-sink as a mixing bus. VLC clips and processed mic audio both feed into it; Discord subscribes to the monitor source that the null-sink exposes automatically.
mkdir -p ~/.config/pipewire/pipewire-pulse.conf.d~/.config/pipewire/pipewire-pulse.conf.d/soundboard-sink.conf
pulse.cmd = [
{ cmd = "load-module" args = "module-null-sink sink_name=soundboard_sink sink_properties=device.description=Soundboard" flags = [ nofail ] }
]Why not
libpipewire-module-loopbackas the mixing bus? A loopback's playback side creates aVirtual Sourcenode (media.class = Audio/Source/Virtual). When that Virtual Source has nonode.target, WirePlumber repeatedly tries to connect it as a driver and fails with-95: Operation not supportedin a tight loop — poisoning the entire PipeWire graph and silencing all ALSA capture system-wide. A null-sink adapter creates a clean.monitorsource with no Virtual Source involved.
Discord only enumerates Audio/Source nodes in its device picker. The raw soundboard_sink.monitor
is class Audio/Source/Monitor and is invisible to Discord. A module-remap-source wraps it as
a proper Audio/Source that Discord can see and select.
~/.config/pipewire/pipewire-pulse.conf.d/soundboard-remap.conf
pulse.cmd = [
{ cmd = "load-module" args = "module-remap-source master=soundboard_sink.monitor source_name=soundboard_monitor source_properties=device.description=SoundboardMonitor" flags = [ nofail ] }
]Use a name without spaces (SoundboardMonitor). Spaces in the args string require shell
escaping that varies by shell and can cause the name to be silently truncated.
This loopback taps processed mic audio from easyeffects_source and feeds it into
soundboard_sink, so Discord hears both voice and soundboard clips from a single source.
mkdir -p ~/.config/pipewire/pipewire.conf.d~/.config/pipewire/pipewire.conf.d/soundboard.conf
context.modules = [
{
name = libpipewire-module-loopback
args = {
node.description = "Mic to Soundboard Mix"
capture.props = {
node.name = "soundboard_mic_capture"
node.target = "easyeffects_source"
stream.dont-remix = true
audio.position = [ FL FR ]
}
playback.props = {
node.name = "soundboard_mic_playback"
node.target = "soundboard_sink"
audio.position = [ FL FR ]
}
}
}
]Routes the raw, unprocessed mic signal directly to the Elgato headphone jack for monitoring on
a second machine connected via a physical cable. Both this loopback and the EasyEffects chain
read from pro-input-0 simultaneously — PipeWire handles multiple consumers without conflict.
~/.config/pipewire/pipewire.conf.d/mic-monitor.conf
context.modules = [
{
name = libpipewire-module-loopback
args = {
node.description = "Mic to XLR Headphone"
capture.props = {
node.name = "mic_monitor_capture"
node.target = "alsa_input.usb-Elgato_Systems_Elgato_Wave_XLR_A01DA349211E0W-00.pro-input-0"
}
playback.props = {
node.name = "mic_monitor_playback"
node.target = "alsa_output.usb-Elgato_Systems_Elgato_Wave_XLR_A01DA349211E0W-00.pro-output-0"
audio.position = [ FL FR ]
}
}
}
]Do not add
audio.positionorstream.dont-remixto the capture props. The Elgato Wave XLR exposes its mic as channelAUX0— notFL,FR, orMONO. Setting any explicitaudio.positionon the capture side causes WirePlumber to fail channel negotiation and silently fall back to a different target node (in testing:soundboard_sink), routing raw mic audio into the soundboard and breaking the mic chain entirely. Omittingaudio.positionlets PipeWire negotiate format directly with the hardware node and connect correctly.The playback side uses
[ FL FR ]because PipeWire maps FL→AUX0, FR→AUX1 on the Elgato headphone output, which produces the correct stereo monitoring signal on the second machine.
Discord (Settings → Voice & Video):
| Setting | Value |
|---|---|
| Input Device | SoundboardMonitor |
| Output Device | Motherboard analog (or your preferred chat output) |
| Noise suppression | Off — EasyEffects handles it |
| Echo cancellation | Off — EasyEffects handles it |
StreamController / VLC — set the audio output sink so clips route to the soundboard:
PULSE_SINK=soundboard_sink vlc ...In StreamController's soundboard plugin, set the PulseAudio sink to soundboard_sink.
Use after creating or modifying any config file, and to start EasyEffects after every reboot:
systemctl --user restart pipewire pipewire-pulse wireplumber && sleep 3 && amixer -c 2 sset 'Front' unmute && easyeffects --service-mode &The sleep 3 is required. EasyEffects must wait for PipeWire to fully initialize the Sound
Blaster node before starting. If it launches too early, its output chain does not link to the
hardware sink and all app audio is silently dropped. Do not reduce this value.
The amixer -c 2 sset 'Front' unmute is required because WirePlumber resets the Sound Blaster's
ALSA Front mixer control to muted when it reinitializes the card. Without this, PipeWire and
EasyEffects will be fully linked and running but produce no audible output. Card 2 is always the
Sound BlasterX AE-5 Plus on this system (verify with cat /proc/asound/cards).
Run this after every restart or reboot to confirm all chains are live:
# 1. Correct defaults
pactl info | grep -E "Default (Sink|Source)"
# Expected:
# Default Sink: alsa_output.pci-0000_05_00.0.analog-stereo
# Default Source: alsa_input.usb-...Wave_XLR...pro-input-0
# 2. No error loops — must return nothing
journalctl --user -u pipewire --since "2 minutes ago" | grep -E "error|-95"
# 3. EasyEffects running
pgrep -a easyeffects
# 4. Soundboard sink and both monitor sources present
pactl list sinks short | grep soundboard_sink
pactl list sources short | grep -E "(soundboard_sink.monitor|soundboard_monitor)"
# 5. Soundboard mic loopback linked
pw-link -l | grep -E "(soundboard_mic|easyeffects_source)"
# Expected: easyeffects_source → soundboard_mic_capture → soundboard_mic_playback → soundboard_sink
# 6. Mic monitor linked (Step 6 only)
pw-link -l | grep -E "(mic_monitor)"
# Expected: pro-input-0 → mic_monitor_capture → mic_monitor_playback → pro-output-0
# 7. EasyEffects signal test — speak into mic while this runs
timeout 3 pw-record --target=easyeffects_source /tmp/ee_test.wav 2>/dev/null
ls -lh /tmp/ee_test.wav
# Pass: ≥ 350 KB Fail: 44 bytes (header only — no signal captured)Mic appears dead after PipeWire restart, but links look correct Restart Discord. It holds stale stream handles after PipeWire restarts and blocks clean reconnection. Closing and reopening Discord resolves it without any PipeWire changes.
-95: Operation not supported error loop in journalctl
A libpipewire-module-loopback is being used as a mixing bus. Its playback side creates a
Virtual Source node that WirePlumber cannot connect as a driver. Replace the loopback mixing bus
with a module-null-sink (see Step 3). Any existing loopback configs used as sinks must be
removed or converted.
soundboard_sink or SoundboardMonitor missing after restart
The pipewire-pulse.conf.d files are only loaded by pipewire-pulse. Restart it:
systemctl --user restart pipewire-pulse
pactl list sources short | grep soundboardSoundboardMonitor not visible in Discord's input device list
Discord filters out Audio/Source/Monitor nodes. Verify soundboard-remap.conf is in
~/.config/pipewire/pipewire-pulse.conf.d/ and that pipewire-pulse has been restarted.
mic_monitor_capture links to wrong node (e.g. soundboard_sink) instead of pro-input-0
The capture props have an explicit audio.position or stream.dont-remix set. Remove both
from the capture side of mic-monitor.conf entirely. See Step 6.
No audio output despite PipeWire running, EasyEffects linked, and Sound Blaster RUNNING
WirePlumber reset the Sound Blaster's ALSA Front control to muted on restart. Fix immediately:
amixer -c 2 sset 'Front' unmuteThis does not require restarting anything. Audio resumes instantly. To persist the unmuted state
across future restarts at the ALSA level: sudo alsactl store 2.
EasyEffects output chain not linked to Sound Blaster after restart
The sleep 3 was skipped or too short. Restart EasyEffects manually:
pkill easyeffects; sleep 3; amixer -c 2 sset 'Front' unmute && easyeffects --service-mode &EasyEffects double-processing loopback streams (soundboard sounds echo-y or over-processed)
The loopback node names are missing from the EasyEffects blocklists. Edit easyeffectsrc:
[StreamInputs] blocklist→ addmic_monitor_capture,soundboard_mic_capture[StreamOutputs] blocklist→ addmic_monitor_playback,soundboard_mic_playback
Elgato pro-input-0 / pro-output-0 nodes missing after reboot
WirePlumber left the Elgato on the off profile. Add the pro-audio entry to default-profile:
systemctl --user stop wireplumber
echo "alsa_card.usb-Elgato_Systems_Elgato_Wave_XLR_A01DA349211E0W-00=pro-audio" >> ~/.local/state/wireplumber/default-profile
systemctl --user start wireplumberWirePlumber uses wrong default sink or source despite default-nodes being set
Check for stale node names in default-nodes. Any name that doesn't match a currently-active
node causes silent misbehavior. Verify with pactl list sinks short / pactl list sources short
and remove or correct stale entries.
| File | Purpose |
|---|---|
~/.local/state/wireplumber/default-nodes |
Default sink + source (survives reboot) |
~/.local/state/wireplumber/default-profile |
Elgato pro-audio profile (survives reboot) |
~/.config/easyeffects/db/easyeffectsrc |
EasyEffects mic chain, output device, blocklists |
~/.config/pipewire/pipewire-pulse.conf.d/soundboard-sink.conf |
Creates soundboard_sink null-sink |
~/.config/pipewire/pipewire-pulse.conf.d/soundboard-remap.conf |
Exposes monitor as Discord-visible source |
~/.config/pipewire/pipewire.conf.d/soundboard.conf |
Loopback: easyeffects_source → soundboard_sink |
~/.config/pipewire/pipewire.conf.d/mic-monitor.conf |
Loopback: raw Elgato mic → Elgato headphone out |