Help us improve
Share bugs, ideas, or general feedback.
From media-os
Routes and introspects Linux audio/video/MIDI graphs with PipeWire: list objects, dump JSON, link ports, play/record, create loopbacks, monitor DSP load and xruns.
npx claudepluginhub damionrashford/media-os --plugin media-osHow this skill is triggered — by the user, by Claude, or both
Slash command
/media-os:audio-pipewire [action][action]The summary Claude sees in its skill listing — used to decide when to auto-load this skill
**Context:** $ARGUMENTS
Guides technical evaluation of code review feedback: read fully, restate for understanding, verify against codebase, respond with reasoning or pushback before implementing.
Share bugs, ideas, or general feedback.
Context: $ARGUMENTS
pwctl.py list)pwctl.py dump)pwctl.py graph)pwctl.py link <src> <dst>)pwctl.py play / record)pwctl.py loopback)pwctl.py top)pwctl.py metadata-set)pw-* commands, or WirePlumber.pw-loopback.Not for: macOS (use audio-coreaudio), Windows (use audio-wasapi), or JACK-specific
workflows (use audio-jack — but note that on modern Linux, jackd is almost always
the PipeWire libjack shim anyway).
Probe the current graph for every node/port/link/device/module/metadata object:
uv run ${CLAUDE_SKILL_DIR}/scripts/pwctl.py list
Filter by type (keeps output manageable on busy desktops):
uv run ${CLAUDE_SKILL_DIR}/scripts/pwctl.py list --kind Node
uv run ${CLAUDE_SKILL_DIR}/scripts/pwctl.py list --kind Port
uv run ${CLAUDE_SKILL_DIR}/scripts/pwctl.py list --kind Link
For the full object-type taxonomy see references/elements.md.
Full JSON snapshot of every object, matching the output format that pw-dump itself
produces:
uv run ${CLAUDE_SKILL_DIR}/scripts/pwctl.py dump
Stream live updates (useful when diagnosing a node that appears only when an app is launched):
uv run ${CLAUDE_SKILL_DIR}/scripts/pwctl.py dump --monitor
Client-side substring filter:
uv run ${CLAUDE_SKILL_DIR}/scripts/pwctl.py dump --filter firefox
Compact ASCII rendering (nodes on the left, their outgoing links on the right):
uv run ${CLAUDE_SKILL_DIR}/scripts/pwctl.py graph
uv run ${CLAUDE_SKILL_DIR}/scripts/pwctl.py graph --kind audio # audio links only
pw-link operates at the port level. Each audio node has one port per channel.
To bridge stereo Firefox → Scarlett playback front-left/front-right:
uv run ${CLAUDE_SKILL_DIR}/scripts/pwctl.py link 'Firefox:output_FL' 'alsa_output.usb-Focusrite_Scarlett:playback_FL'
uv run ${CLAUDE_SKILL_DIR}/scripts/pwctl.py link 'Firefox:output_FR' 'alsa_output.usb-Focusrite_Scarlett:playback_FR'
List existing links:
uv run ${CLAUDE_SKILL_DIR}/scripts/pwctl.py links
Remove a link:
uv run ${CLAUDE_SKILL_DIR}/scripts/pwctl.py unlink 'Firefox:output_FL' 'alsa_output.usb-Focusrite_Scarlett:playback_FL'
You can pass numeric object ids (from list) instead of names.
pw-cat is the unified playback+record tool. pw-play/pw-record are symlinks:
uv run ${CLAUDE_SKILL_DIR}/scripts/pwctl.py play path/to/track.wav
uv run ${CLAUDE_SKILL_DIR}/scripts/pwctl.py play path/to/track.wav --target alsa_output.usb-...
uv run ${CLAUDE_SKILL_DIR}/scripts/pwctl.py record captured.wav --duration 10
record is implemented as pw-cat --record. PipeWire negotiates format from the
file extension (WAV / FLAC / RAW). For MIDI use pw-midiplay / pw-midirecord directly
(not wrapped here — they take SMF files).
pw-loopback creates a virtual source↔sink pair that any app can target. Classic
use case: capture system audio into OBS by recording from the loopback source.
uv run ${CLAUDE_SKILL_DIR}/scripts/pwctl.py loopback --name MyVirtualCable --channels 2
Bridge specific endpoints:
uv run ${CLAUDE_SKILL_DIR}/scripts/pwctl.py loopback \
--name StreamMix \
--capture 'alsa_input.usb-...' \
--playback 'alsa_output.usb-...'
pw-loopback runs in the foreground; kill with SIGINT when done. To make one
permanent, add a module fragment under ~/.config/pipewire/pipewire.conf.d/ or a
WirePlumber Lua script.
uv run ${CLAUDE_SKILL_DIR}/scripts/pwctl.py top # curses TUI
uv run ${CLAUDE_SKILL_DIR}/scripts/pwctl.py top --batch-mode --iterations 5
Columns: S(tate), id, QUANTum, RATE, WAIT, BUSY, W/Q (wait/quantum%), B/Q (busy/quantum%), ERR (xruns), FORMAT, NAME.
Rule of thumb: if B/Q > ~70% a node can't keep up with its quantum; drop quantum or raise the period.
pw-metadata reads/writes the shared metadata store. WirePlumber and
pipewire-pulse both consult it.
Read the current defaults:
uv run ${CLAUDE_SKILL_DIR}/scripts/pwctl.py metadata-get --id 0
Change the default audio sink (note the JSON-wrapped value — WirePlumber requires it):
uv run ${CLAUDE_SKILL_DIR}/scripts/pwctl.py metadata-set \
--id 0 --key default.configured.audio.sink \
--value '{ "name": "alsa_output.usb-Focusrite_Scarlett" }' \
--type Spa:String:JSON
To see WirePlumber's config search paths (where to drop a Lua fragment to persist changes):
uv run ${CLAUDE_SKILL_DIR}/scripts/pwctl.py wireplumber-config
pipewire-media-session is
deprecated and won't match the behavior of a current WirePlumber-based distro.
Don't look up Lua/Spa-Json config against the old media-session docs.jackd shim vs real JACK. On most modern Linux desktops the jackd
binary on $PATH is PipeWire's libjack shim — starting it a second time does not
change quantum/period/samplerate. To raise/lower quantum under PipeWire use
pw-metadata -n settings 0 clock.force-quantum <N> (set 0 to let WirePlumber pick).pw-link "node name:port name" "other:port". Numeric ids are safer for scripts.channelmap/loopback). PipeWire
won't auto-broadcast.pw-dump without --monitor is a snapshot, not a live stream. For an app
that only creates its nodes on startup, run --monitor and then launch the app.pw-cat --record does not take --duration. This wrapper wraps with timeout
to bound a recording. Without it, record until SIGINT.pw-metadata only persists if WirePlumber policy
saves it. For true persistence, add a WirePlumber Lua fragment under
~/.config/wireplumber/main.lua.d/.pw-loopback exits as soon as you interrupt it. For persistent virtual
cables, load the libpipewire-module-loopback module from a config fragment
instead of running the CLI.pw-top measures graph time, not CPU %. A B/Q near 100% means "this
node is burning almost all of its scheduled quantum", which may or may not be
the CPU bottleneck.alsa_output.usb-Focusrite_Scarlett) in scripts, not ids.Spa:String:JSON type hint. Plain strings
don't deserialize — WirePlumber will silently ignore the write.pw-jack <app> vs jackd. pw-jack firefox launches Firefox against
PipeWire's libjack shim for the process only — useful for testing JACK apps
without replacing system jackd.uv run ${CLAUDE_SKILL_DIR}/scripts/pwctl.py list --kind Port # find port names
uv run ${CLAUDE_SKILL_DIR}/scripts/pwctl.py link 'Firefox:output_FL' 'alsa_output.usb-Focusrite_Scarlett:playback_FL'
uv run ${CLAUDE_SKILL_DIR}/scripts/pwctl.py link 'Firefox:output_FR' 'alsa_output.usb-Focusrite_Scarlett:playback_FR'
uv run ${CLAUDE_SKILL_DIR}/scripts/pwctl.py loopback --name OBS_Cable --channels 2
# Point OBS's "Audio Input Capture" at OBS_Cable, then route apps to it:
uv run ${CLAUDE_SKILL_DIR}/scripts/pwctl.py link 'Firefox:output_FL' 'OBS_Cable:input_FL'
uv run ${CLAUDE_SKILL_DIR}/scripts/pwctl.py link 'Firefox:output_FR' 'OBS_Cable:input_FR'
uv run ${CLAUDE_SKILL_DIR}/scripts/pwctl.py top --batch-mode --iterations 20
# Look at the ERR column. Then raise the quantum:
pw-metadata -n settings 0 clock.force-quantum 1024
uv run ${CLAUDE_SKILL_DIR}/scripts/pwctl.py links
uv run ${CLAUDE_SKILL_DIR}/scripts/pwctl.py metadata-set \
--id 0 --key default.configured.audio.sink \
--value '{ "name": "alsa_output.usb-Focusrite_Scarlett" }' \
--type Spa:String:JSON
pw-cli: command not found / pw-link: command not foundCause: PipeWire is not installed or $PATH missing.
Fix: Install the distro package (Fedora: pipewire, pipewire-utils;
Debian/Ubuntu: pipewire-bin, wireplumber). Confirm with pw-cli --version.
pw-link exits with "port not found"Cause: Port name string doesn't match, or the source/sink doesn't exist yet.
Fix: Run pwctl.py list --kind Port to see the exact names. Quote shell-special
chars. Or pass numeric ids from list.
pw-dump returns empty []Cause: PipeWire daemon not running under the current session. On some minimal
installs the service isn't autostarted.
Fix: systemctl --user start pipewire.service pipewire-pulse.service wireplumber.service
and retry.
Cause: pw-loopback CLI is tied to the foreground process.
Fix: Persist via libpipewire-module-loopback in a config fragment under
~/.config/pipewire/pipewire.conf.d/, or a WirePlumber Lua fragment.
Cause: WirePlumber's default-nodes.lua did not persist the change.
Fix: Write a Lua persistence fragment under ~/.config/wireplumber/main.lua.d/
instead of relying solely on pw-metadata.
references/elements.md.