Skip to content

Instantly share code, notes, and snippets.

@nullconfig
Last active June 16, 2026 20:14
Show Gist options
  • Select an option

  • Save nullconfig/a723dd530295cda8a2cd0e1ec937fc39 to your computer and use it in GitHub Desktop.

Select an option

Save nullconfig/a723dd530295cda8a2cd0e1ec937fc39 to your computer and use it in GitHub Desktop.
Initial Audio Setup

PipeWire Streaming Audio — Complete Setup Guide

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


What This Achieves

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

Hardware

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

Prerequisites

  • PipeWire + WirePlumber running (not PulseAudio)
  • pipewire-pulse installed (PulseAudio compatibility layer)
  • EasyEffects installed and configured with your desired mic plugin chain
  • pw-link and pactl available (in pipewire and libpulse packages)

Verify PipeWire is active:

pactl info | grep "Server Name"
# Expected: Server Name: PulseAudio (on PipeWire ...)

Step 1 — Set WirePlumber Defaults

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-audio

The 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_source or any virtual source. EasyEffects reads the system default source at startup and writes it back to easyeffectsrc. 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 with pactl list sinks short and pactl list sources short any time you change hardware.


Step 2 — Configure EasyEffects

~/.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=true

The 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.


Step 3 — Create the Soundboard Mixing Bus

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-loopback as the mixing bus? A loopback's playback side creates a Virtual Source node (media.class = Audio/Source/Virtual). When that Virtual Source has no node.target, WirePlumber repeatedly tries to connect it as a driver and fails with -95: Operation not supported in a tight loop — poisoning the entire PipeWire graph and silencing all ALSA capture system-wide. A null-sink adapter creates a clean .monitor source with no Virtual Source involved.


Step 4 — Expose the Monitor Source to Discord

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.


Step 5 — Route Processed Mic into the Soundboard

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 ]
      }
    }
  }
]

Step 6 — Raw Mic Monitor to Second Device (optional)

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.position or stream.dont-remix to the capture props. The Elgato Wave XLR exposes its mic as channel AUX0 — not FL, FR, or MONO. Setting any explicit audio.position on 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. Omitting audio.position lets 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.


Step 7 — App Settings

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.


Restart Procedure

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).


Verification

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)

Troubleshooting

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 soundboard

SoundboardMonitor 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' unmute

This 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 → add mic_monitor_capture,soundboard_mic_capture
  • [StreamOutputs] blocklist → add mic_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 wireplumber

WirePlumber 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 Inventory

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment