| Crates.io | pw-splitter |
| lib.rs | pw-splitter |
| version | 0.1.0 |
| created_at | 2025-12-24 00:42:18.201724+00 |
| updated_at | 2025-12-24 00:42:18.201724+00 |
| description | PipeWire audio routing TUI for splitting audio streams |
| homepage | |
| repository | https://github.com/Sewer56/pw-splitter |
| max_upload_size | |
| id | 2002591 |
| size | 102,433 |
A TUI application for PipeWire that lets you capture audio from an application at full volume for recording while independently controlling your local listening volume.
When you're recording or streaming with OBS, you often want to:
In PipeWire, adjusting an application's volume affects all destinations. If you turn down a game's volume to save your hearing, your recording also gets quieter.
pw-splitter creates two parallel audio paths from your source application - one to your recording software at full volume, and one to your speakers with adjustable volume.
BEFORE (volume affects everything):
[Game Audio] ──────────────────────────────> [Speakers]
│ (loud!)
│
└─────────────────────────────────────> [OBS Recording]
(also loud!)
Turning down the game makes BOTH quieter.
AFTER (independent volume control):
[Game Audio] ───┬──> [To Recording] ──> [OBS Mic/Aux]
│ (full volume)
│
└──> [To Local] ──> [Speakers]
(adjustable)
Adjust "To Local" volume in pwvucontrol - recording stays at 100%.
First, check your package manager (e.g., dnf install pw-splitter, pacman -S pw-splitter) for a pre-packaged version.
Download the latest release from GitHub Releases. Pre-built binaries are available for:
linux-x64.zip (x86_64)linux-x86.zip (i686)linux-arm64.zip (aarch64)# Download and run (example for x86_64)
wget https://github.com/Sewer56/pw-splitter/releases/latest/download/linux-x64.zip
unzip linux-x64.zip
chmod +x pw-splitter
./pw-splitter
If you have Rust installed, install from crates.io:
cargo install pw-splitter
# Move to source directory
cd src
# Build and run the project
cargo run --release
# Use arrow keys to select:
# 1. Source application (e.g., "Dolphin Emulator")
# 2. Recording destination (e.g., "OBS [Mic/Aux]")
# 3. Press Enter to confirm
# Adjust local volume in pwvucontrol
# Look for the loopback with "Local" in the name
pw-splitter
| Key | Action |
|---|---|
↑/↓ or j/k |
Navigate list |
Enter |
Select / Confirm |
Esc |
Go back |
r |
Refresh list |
q |
Quit |
pw-splitter list # Show active splits
pw-splitter stop <name> # Stop a specific split
pw-splitter stop-all # Stop all splits
After setting up a split for Dolphin Emulator to OBS [Mic/Aux]:
┌────────────────────────────────────────────┐
│ Dolphin Emulator [Dolphin Audio Output] │ ◄── Source application
└──────────┬─────────────────────┬───────────┘
│ │
▼ ▼
┌────────────────────┐ ┌────────────────────┐
│ Dolphin Emulator │ │ Dolphin Emulator │
│ -> Local │ │ -> OBS │
│ [...Local input] │ │ [...OBS input] │ ◄── Loopback capture sides
└─────────┬──────────┘ └──────────┬─────────┘
│ │
▼ ▼
┌────────────────────┐ ┌────────────────────┐
│ Dolphin Emulator │ │ Dolphin Emulator │
│ -> Local │ │ -> OBS │
│ [...Local output] │ │ [...OBS output] │ ◄── Loopback playback sides
└─────────┬──────────┘ └──────────┬─────────┘
│ │
▼ ▼
┌────────────────────┐ ┌────────────────────┐
│ USB Audio Speakers │ │ OBS-1 [Mic/Aux] │
└────────────────────┘ └────────────────────┘
| Node Name | Type | Purpose |
|---|---|---|
Dolphin Emulator -> Local [... input] |
Loopback (capture) | Captures from source app |
Dolphin Emulator -> Local [... output] |
Loopback (playback) | Sends to speakers - adjust this volume! |
Dolphin Emulator -> OBS [... input] |
Loopback (capture) | Captures from source app |
Dolphin Emulator -> OBS [... output] |
Loopback (playback) | Sends to OBS at full volume |
In pwvucontrol or qpwgraph, find the loopback with "Local" in the name and adjust its volume. The recording to OBS will stay at 100% regardless of this setting.
| Term | Meaning |
|---|---|
| Sink | An audio destination (like speakers). Applications "sink" their audio into it. |
| Source | An audio origin (like a microphone). Applications capture audio from it. |
| Node | Any audio endpoint in PipeWire - could be an app, device, or virtual component. |
| Loopback | Takes audio from one place and sends it to another. Like an audio cable. |
| Stream/Output/Audio | An application playing audio (e.g., game, music player). |
| Stream/Input/Audio | An application recording audio (e.g., OBS audio capture). |
The split works by creating two pw-loopback instances that both capture from the source application:
┌──────────────┐ ┌─────────────────┐ ┌──────────────┐
│ Source │ │ pw-loopback │ │ Speakers │
│ Application │────▶│ (to Local) │────▶│ │
│ │ │ [adjustable] │ │ │
│ │ └─────────────────┘ └──────────────┘
│ │
│ │ ┌─────────────────┐ ┌──────────────┐
│ │ │ pw-loopback │ │ OBS │
│ │────▶│ (to Recording) │────▶│ [Mic/Aux] │
│ │ │ [full volume] │ │ │
└──────────────┘ └─────────────────┘ └──────────────┘
Create two loopbacks with node.autoconnect=false on both capture and playback sides:
# Recording loopback (to OBS)
pw-loopback \
--capture-props='media.class=Audio/Sink node.name=MyApp_to_Recording node.description="MyApp -> OBS" node.autoconnect=false stream.capture.sink=true' \
--playback-props='media.class=Stream/Output/Audio node.name=MyApp_to_Recording node.description="MyApp -> OBS" node.autoconnect=false'
# Local loopback (to speakers)
pw-loopback \
--capture-props='media.class=Audio/Sink node.name=MyApp_to_Local node.description="MyApp -> Local" node.autoconnect=false stream.capture.sink=true' \
--playback-props='media.class=Stream/Output/Audio node.name=MyApp_to_Local node.description="MyApp -> Local" node.autoconnect=false'
Key options:
node.autoconnect=false - Prevents PipeWire from auto-connectingstream.capture.sink=true - Captures from sinks (apps), not sources (mics)Disconnect source from original outputs:
# Remove existing links to speakers
pw-link -d "Dolphin Emulator:output_FL" "speakers:playback_FL"
pw-link -d "Dolphin Emulator:output_FR" "speakers:playback_FR"
# Also remove any existing links to the recording destination
pw-link -d "Dolphin Emulator:output_FL" "OBS:input_FL"
pw-link -d "Dolphin Emulator:output_FR" "OBS:input_FR"
Connect source to both loopback capture inputs:
# Source → Recording loopback capture
pw-link "Dolphin Emulator:output_FL" "MyApp_to_Recording:input_FL"
pw-link "Dolphin Emulator:output_FR" "MyApp_to_Recording:input_FR"
# Source → Local loopback capture
pw-link "Dolphin Emulator:output_FL" "MyApp_to_Local:input_FL"
pw-link "Dolphin Emulator:output_FR" "MyApp_to_Local:input_FR"
Connect loopback playback outputs to destinations:
# Recording loopback → OBS input (using port IDs to avoid node name ambiguity)
pw-link 85 118 # port ID 85 = loopback output_FL, port ID 118 = OBS input_FL
pw-link 86 119 # port ID 86 = loopback output_FR, port ID 119 = OBS input_FR
# Local loopback → Speakers (can use names since speakers have unique names)
pw-link "MyApp_to_Local:output_FL" "speakers:playback_FL"
pw-link "MyApp_to_Local:output_FR" "speakers:playback_FR"
Note: Port IDs are used for OBS because multiple OBS inputs share node.name="OBS",
making port names like OBS:input_FL ambiguous.
OBS audio inputs are Stream/Input/Audio nodes - they're capture streams that read FROM sinks, not sinks themselves. You can't target them with pw-loopback's target.object on the playback side.
Additionally, multiple OBS inputs share the same node.name ("OBS"), making port names like OBS:input_FL ambiguous. We use port IDs directly to ensure we connect to the correct OBS input.
Active splits are stored in /tmp/pw-splitter/<name>.json:
{
"name": "DolphinEmulator_Split",
"source_node_id": 158,
"recording_loopback_name": "DolphinEmulator_to_Recording",
"local_loopback_name": "DolphinEmulator_to_Local",
"recording_dest_node_id": 118,
"loopback_to_recording_pid": 12345,
"loopback_to_local_pid": 12346,
"original_links": [
{"output_port": "Dolphin Emulator:output_FL", "input_port": "speakers:playback_FL"}
]
}
This enables:
When stopping a split:
pw-loopback processescargo build --release
Dependencies:
ratatui - TUI frameworkcrossterm - Terminal handlingserde / serde_json - State serializationargh - CLI parsingthiserror - Error handlingRuntime requirements:
pw-link, pw-loopback, pw-dump commandsMIT
I needed something easy to use for a one-off stream; and maybe future use. Given that an LLM did most of the heavy lifting, I'd rather release this for absolutely free.
For step-by-step development guidance, see the Developer Manual.
We welcome contributions! See the Contributing Guide for details.