PipeWire + WirePlumber desktop を対象に stream・policy・node・kernel hardware のどこで失敗したか調べる。wpctl・pactl・ALSA の ID は互換でも再起動後に安定でもない。
policy 管理は音声経路の直列要素ではない
実線は PulseAudio protocol の再生例。native client は直接 PipeWire へ入る。WirePlumber は link・profile・policy を制御し音声データの直列 hop ではない。Bluetooth は別 backend、図は ALSA の経路。
現 desktop session を先に確認する
systemctl --user status pipewire pipewire-pulse wireplumber --no-pager
問題の desktop user で実行し、sudo で root の user manager を見ない。unit 名は環境に合わせる。
wpctl status -n
現 ID・名前を記録し、再起動・再接続後は取り直す。
pactl info
Server Name/String を想定する互換 server と照合し、接続・session 失敗を先に解決する。
対象の種類ごとに確認する
pactl list short sinks
pactl list short sources
.monitor は通常出力 mix で、物理 mic ではない。
pactl list short sink-inputs
native PipeWire stream は現れない場合もある。空なら再生中か・protocol を確認する。
aplay -l
cat /proc/asound/cards
hardware 調査向け。Bluetooth が ALSA にないだけで故障としない。
server の識別、sink、stream を確認する。wpctl の星は既定 node で全 stream の接続先を保証しない。ALSA 列挙だけでは profile・mute・route は不明。
既定機器と固定 stream を分ける
図を準備しています
既定変更後も音声が元に残る理由既定は自動接続の候補で、既存 stream は独自に固定できる。既定変更・固定・明示移動を分けて示す。
{"id":"ops-media-routing","title":"既定変更後も音声が元に残る理由","summary":"既定は自動接続の候補で、既存 stream は独自に固定できる。既定変更・固定・明示移動を分けて示す。","height":600,"html":"<main><p class=\"quiet\" data-i18n=\"scope\"></p><h3 data-i18n=\"default\"></h3><div class=\"presets\" role=\"group\"><button type=\"button\" data-device=\"speaker\" aria-pressed=\"false\" data-i18n=\"speaker\"></button><button type=\"button\" data-device=\"headphone\" aria-pressed=\"false\" data-i18n=\"headphone\"></button></div><div class=\"checks\"><label><input id=\"pin\" type=\"checkbox\" ><span data-i18n=\"pin\"></span></label></div><div class=\"pair\"><section class=\"card \"><h3 data-i18n=\"default\"></h3><strong id=\"default\"></strong></section><section class=\"card \"><h3 data-i18n=\"stream\"></h3><strong id=\"stream\"></strong></section></div><div class=\"row\" id=\"route\"></div><div class=\"controls\"><button type=\"button\" data-action=\"move\" data-i18n=\"move\"></button></div><div class=\"explain\"><div class=\"stable \"><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"hint\"></p><p id=\"message\" aria-live=\"polite\"></p></div></div></main>\n","css":"main {\n display: grid;\n gap: 18px;\n min-width: 0;\n}\np {\n overflow-wrap: anywhere;\n}\n.controls {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n align-items: center;\n}\n.presets {\n display: flex;\n flex-wrap: wrap;\n}\n.presets button {\n flex: 1 1 85px;\n font-size: 15px;\n}\n.cards {\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: 10px;\n}\n.card {\n border: 1px solid var(--rule);\n border-radius: 10px;\n padding: 14px;\n min-width: 0;\n background: var(--surface);\n}\n.card h3 {\n font-size: 15px;\n color: var(--muted);\n margin-bottom: 8px;\n}\n.card strong {\n font-size: 23px;\n font-weight: 500;\n overflow-wrap: anywhere;\n}\n.code {\n font-family: var(--viz-font-mono);\n font-size: 15px;\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n.stable {\n display: grid;\n min-width: 0;\n}\n.stable > * {\n grid-area: 1/1;\n margin: 0;\n}\n.stable > .reserve {\n visibility: hidden;\n pointer-events: none;\n}\n.explain {\n border-left: 3px solid var(--accent);\n padding: 12px 14px;\n background: var(--accent-soft);\n}\n.arrow {\n text-align: center;\n color: var(--accent);\n font-size: 25px;\n}\n.row {\n padding: 12px;\n border: 1px solid var(--rule);\n border-radius: 8px;\n min-width: 0;\n}\n.row.on {\n border-color: var(--accent);\n background: var(--accent-soft);\n}\n.row.off {\n color: var(--muted);\n}\n.bar {\n height: 18px;\n background: var(--surface);\n border: 1px solid var(--rule);\n border-radius: 5px;\n overflow: hidden;\n margin-top: 8px;\n}\n.bar > i {\n display: block;\n height: 100%;\n background: var(--accent);\n width: 0;\n transition: width 0.22s;\n}\n.checks {\n display: grid;\n gap: 8px;\n}\n.checks label {\n display: flex;\n gap: 10px;\n align-items: center;\n padding: 9px;\n border: 1px solid var(--rule);\n border-radius: 7px;\n font-size: 16px;\n}\n.checks input {\n width: 18px;\n height: 18px;\n accent-color: var(--accent);\n flex-shrink: 0;\n}\nlabel.range {\n display: block;\n font-size: 16px;\n}\nlabel.range output {\n float: right;\n font-variant-numeric: tabular-nums;\n}\n.pair {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 10px;\n}\n.flow {\n display: grid;\n gap: 6px;\n}\n.quiet {\n font-size: 14px;\n color: var(--muted);\n}\n.badge {\n display: inline-block;\n padding: 2px 8px;\n border-radius: 5px;\n background: var(--accent-soft);\n color: var(--accent);\n}\nbutton {\n overflow-wrap: anywhere;\n}\n.large {\n font-size: 32px;\n color: var(--accent);\n font-weight: 500;\n}\n.layers {\n display: grid;\n gap: 12px;\n}\n.layer {\n border-left: 3px solid var(--rule);\n padding-left: 12px;\n}\n.layer.on {\n border-color: var(--accent);\n}\n@media (max-width: 520px) {\n .cards,\n .pair {\n grid-template-columns: 1fr;\n }\n .card {\n padding: 12px;\n }\n .card strong {\n font-size: 22px;\n }\n}\n","js":"const $ = (s) => document.querySelector(s);\nconst $$ = (s) => [...document.querySelectorAll(s)];\nconst t = (k) => viz.t(k);\nconst put = (id, v) => ($(\"#\" + id).textContent = v);\nconst say = (k) => put(\"message\", t(k));\nconst state = (v) => ($(\"main\").dataset.state = JSON.stringify(v));\nconst press = (key, v) =>\n $$(\"[data-\" + key + \"]\").forEach((b) =>\n b.setAttribute(\"aria-pressed\", String(b.dataset[key] === v)),\n );\nlet def = \"speaker\",\n stream = \"speaker\";\nfunction draw() {\n put(\"default\", t(def));\n put(\"stream\", t(stream));\n put(\"route\", \"♪ → \" + t(stream));\n press(\"device\", def);\n state({ def, stream, pinned: $(\"#pin\").checked });\n}\n$$(\"[data-device]\").forEach(\n (b) =>\n (b.onclick = () => {\n def = b.dataset.device;\n if (!$(\"#pin\").checked) stream = def;\n draw();\n }),\n);\n$(\"#pin\").onchange = () => {\n if (!$(\"#pin\").checked) stream = def;\n draw();\n};\n$(\"[data-action=move]\").onclick = () => {\n stream = def;\n $(\"#pin\").checked = true;\n draw();\n};\nsay(\"hint\");\ndraw();\n","audio":false,"strings":{"default":"既定先を設定","headphone":"headphone","hint":"stream を固定してから既定を変えると行先が分かれる。明示移動はこの stream だけを変える。","move":"stream を現在の既定先へ","pin":"既存 stream を固定","scope":"二機器・一 stream。模型では未固定は既定に追従、固定は維持。実動作は policy・アプリに依存。","speaker":"speaker","stream":"既存 stream の行先"}}
音量・mute を個別に選ぶ
wpctl get-volume @DEFAULT_AUDIO_SINK@
wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+ --limit 1.0
加算であり 1.05 倍ではない。get-volume で再確認し、route の問題とは分ける。
wpctl set-mute @DEFAULT_AUDIO_SINK@ 0
0 は解除、1 は mute。toggle は shortcut 向けで、調査では明示状態が再現しやすい。
wpctl set-mute @DEFAULT_AUDIO_SOURCE@ 1
入出力は別。本操作は再生修復の必須手順ではない。録音再開は 0 と権限・入力元を確認する。
既定先の変更と既存 stream の移動
wpctl set-default 42
42 を現 sink ID にする。旧既定先を記録し、戻す時も再照会する。星印と新規再生で確認する。
pactl list short sink-inputs
application と照合する。wpctl sink ID とは別。
pactl move-sink-input 73 YOUR_SINK_NAME
現 ID と sinks 一覧の名前を使う。再一覧・試聴で確認し、stream 再作成後は方針が変わり得る。
profile が合わない場合だけ切り替える
pactl list cards
pactl set-card-profile YOUR_CARD_NAME YOUR_PROFILE_NAME
両方 list cards の実名を使い旧 profile を記録する。再生中断・ID 変更があるため再照会し、戻す時は旧実名を使う。
42 は現在の wpctl sink、73 は pactl sink-input。set-default は自動接続する新 stream の既定先で既存移動は policy 等に依存する。特定 stream は明示移動する。profile は利用可能一覧から選び、通話用では音質・capture 能力が変わる。
失敗層の根拠を集めてから再起動
症状発生時の証拠を残す
journalctl --user -u pipewire -u pipewire-pulse -u wireplumber -b -n 150 --no-pager
pw-top
負荷を再現し ERR 増加・対象 node を見る。q で終了。一度の高値だけで原因としない。
wpctl inspect 42
ID を再確認し node.name・device 関連・media type を読む。
pactl list sinks
journalctl -k -b -n 100 --no-pager
| 現象 | 対応 |
| Dummy Output のみ | 列挙・driver/firmware・権限・policy log |
| sink と stream はあるが無音 | target・stream/device mute・profile/port・mixer・配線 |
| 途切れ・雑音 | pw-top error・負荷と時刻を照合してから rate/quantum を調整 |
| Bluetooth 無音 | Bluetooth で接続・profile を確認 |
根拠がある場合に wireplumber を再起動し必要な層だけ拡げる。アプリの再接続が必要になり得る。aplay hw の失敗は形式や占有でも起こり、speaker-test default は PipeWire 経由かもしれない。124 は timeout の証拠だけ。
入力元を選び container と codec を確認
録音:入力を選び、停止して file を確認する
pactl list short sources
parecord --file-format=wav --device=YOUR_SOURCE_NAME recording.wav
入力名・未使用 file 名を指定する。Ctrl-C で止め shell が戻ってから次へ進み、録音と後処理を一括貼付しない。
ffprobe -v error -show_format -show_streams -of json recording.wav
解析・期待する長さを確認して試聴する。存在だけで正しい入力の録音とは限らない。
変換:入力と local encoder を先に確認する
ffprobe -v error -show_format -show_streams -of json input.mp4
ffmpeg -hide_banner -encoders
以下は libx264・aac。hardware encoder は device・driver・権限も必要。
ffmpeg -n -i input.mp4 -map 0:v:0 -map "0:a:0?" \
-c:v libx264 -crf 23 -c:a aac output.mp4
-n は上書き拒否、-map は stream 選択、? は音声なしを許す。CRF が小さいほど通常品質・容量が増す。全字幕・添付・音声を保持する例ではない。
ffprobe -v error -show_format -show_streams -of json output.mp4
container 変更だけなら互換性を確認して新 file へ -c copy を検討する。再 encoding とは別方式。
録音は Ctrl-C で終了。monitor は出力 mix、通常 source は入力。raw parec を拡張子だけで WAV にしない。FFmpeg の例は encoder が必要で CRF23 は万能品質値ではない。copy も container 互換性が必要。
Wayland は desktop 対応 portal/recorder を使う。wf-recorder は対応 screencopy が必要で全 compositor 共通ではない。音声は実 source 名を指定する。media key は現在の desktop 構文で wpctl/playerctl に結ぶ。firmware は kernel の欠落ファイルを特定する。
WirePlumber wpctl · Linking policy · FFmpeg · wf-recorder