以 PipeWire + WirePlumber 桌面为主,先定位应用流、策略、设备节点还是内核硬件层。wpctl、pactl 和 ALSA 使用不同对象与 ID,数字不能混用,也不能跨重启当稳定标识。
策略管理不在音频数据的串行路径中
实线示例是 PulseAudio 协议应用的播放路径;原生 PipeWire 应用可直接进入 PipeWire。WirePlumber 决定连接、profile 和策略,不承载每段音频数据;蓝牙输出还经过 BlueZ 等对应后端,图中只画 ALSA 硬件路径。
先检查当前桌面会话
systemctl --user status pipewire pipewire-pulse wireplumber --no-pager
在发生问题的桌面用户会话执行,不加 sudo 切到 root 的 user manager。服务名称依发行版调整。
wpctl status -n
记录目标 sink/source 的当前 ID 和名称;重启或重插后重新获取,不复用旧数字。
pactl info
Server Name 与 Server String 应符合当前 PipeWire 兼容层;连接失败先处理服务/会话环境。
按对象分别查看
pactl list short sinks
pactl list short sources
带 .monitor 的名称通常对应输出混音,不是物理麦克风。
pactl list short sink-inputs
原生 PipeWire 流不一定出现在这里;空列表先确认应用是否正在输出以及使用的协议。
aplay -l
cat /proc/asound/cards
只有需要进一步定位硬件时使用;蓝牙设备不应一概按 ALSA 声卡缺失判故障。
先看 Server Name 是否为预期兼容服务、目标 sink 是否存在、流是否出现。wpctl status 的星号代表默认节点,不代表所有已有流都已接到它。ALSA 能枚举声卡不代表 profile、静音和应用路由正确。
默认设备与已固定的流分开操作
正在呈现知识画面
改变默认设备,为何旧音频还留在原处默认节点是自动连接的候选;已有流可固定到自己的目标。模型把改变默认、固定流、显式移动流三个动作分开。
{"id":"ops-media-routing","title":"改变默认设备,为何旧音频还留在原处","summary":"默认节点是自动连接的候选;已有流可固定到自己的目标。模型把改变默认、固定流、显式移动流三个动作分开。","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":"耳机","hint":"先固定流,再切换默认设备:两个目标可以不同。显式移动只改变这条流,不改变默认设备。","move":"把流移到当前默认","pin":"固定现有流的目标","scope":"两台设备、一条流。为便于比较,本模型约定未固定流跟随默认,固定流保持目标;真实行为还取决于 WirePlumber/应用策略。","speaker":"扬声器","stream":"现有流的目标"}}
音量与静音:每项独立选择
wpctl get-volume @DEFAULT_AUDIO_SINK@
wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+ --limit 1.0
相对增量不是音量乘以 1.05;执行后再 get-volume 核对。它不会自动修复错误的输出路由。
wpctl set-mute @DEFAULT_AUDIO_SINK@ 0
0 取消、1 静音;toggle 适合快捷键,诊断时明确状态更容易复现。
wpctl set-mute @DEFAULT_AUDIO_SOURCE@ 1
输入和输出是不同对象,不要把本条当成恢复声音的必经步骤。恢复录音用 0,并检查应用权限和输入来源。
修改默认目标,或移动现有流
wpctl set-default 42
42 替换为当前 wpctl status 的 sink ID;记录原默认节点,回退时重新查询并选回它。操作后看星号位置并启动一次新播放。
pactl list short sink-inputs
结合应用名称确认对象;它不是 wpctl 的 sink ID。
pactl move-sink-input 73 YOUR_SINK_NAME
73 换成上条 ID,目标取自 pactl list short sinks 的名称。重新列出流看目标字段,并实际试听;应用重建流后可能采用新的策略。
只有 profile 不合适时才切换
pactl list cards
pactl set-card-profile YOUR_CARD_NAME YOUR_PROFILE_NAME
两名称均取自 list cards,先记录原 profile。切换可能中断播放并改变节点 ID;之后重新查 sinks/sources 与应用路由,回退也用原 profile 的真实名称。
42 来自当前 wpctl 的 sink,73 来自当前 pactl 的 sink-input,YOUR_SINK_NAME 来自 pactl sinks。set-default 影响需自动连接的新流;现有流是否迁移还受策略、保存目标和应用行为影响。要确定地移动某条兼容流,用其 sink-input ID 或 pavucontrol。profile 必须从 pactl list cards 的可用列表选取;切到通话 profile 可能改变带宽与麦克风能力。
在失败层收集证据后再重启
先保留故障发生时的证据
journalctl --user -u pipewire -u pipewire-pulse -u wireplumber -b -n 150 --no-pager
pw-top
保持复现负载,观察 ERR 是否持续增加以及变化对应的节点;q 退出。单次高值不能单独证明哪一层故障。
wpctl inspect 42
先重新确认 ID;看 node.name、设备关联和媒体类型,不能把旧 ID 对应的新对象当成原设备。
pactl list sinks
journalctl -k -b -n 100 --no-pager
| 现象 | 下一步 |
| 只有 Dummy Output | 查声卡枚举、驱动/固件、设备权限和 WirePlumber 日志 |
| 有 sink、有流但无声 | 查流目标、两级静音、profile/port、ALSA mixer 与物理连接 |
| 卡顿 / 爆音 | 用 pw-top 的错误与负载变化对齐时间,不先随意调采样率和 quantum |
| 蓝牙无声 | 先确认连接与 profile,见 蓝牙 |
确认需要重启后用 systemctl --user restart wireplumber,只有核心或兼容层异常才扩展到 pipewire/pipewire-pulse;这会影响活动会话,应用可能要重连。aplay -D hw 可能因格式或独占失败,speaker-test 的 default 也可能经过 PipeWire,二者都不是单独的硬件好坏判决。timeout 命令返回 124 仅表示超时,不直接定位音频守护进程死锁。
先选输入源,再验证容器与编码
录音:选择来源、停止、再检查文件
pactl list short sources
parecord --file-format=wav --device=YOUR_SOURCE_NAME recording.wav
替换来源名并选未使用的文件名;该命令持续运行,录制代表性内容后按 Ctrl-C,等待返回 shell,再执行下一条。不要一次粘贴录制与后处理。
ffprobe -v error -show_format -show_streams -of json recording.wav
确认文件可解析、时长符合录制窗口并试听实际内容;文件存在不说明录到了正确来源。
转码:先查输入和本机编码器
ffprobe -v error -show_format -show_streams -of json input.mp4
ffmpeg -hide_banner -encoders
下面使用 libx264 与 aac;硬件编码器还要求匹配设备、驱动和权限,列出名称不等于可用。
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 明确取流,问号允许没有音频;CRF 越小通常质量和文件体积越高。此例不保留所有字幕/附件/音轨,需求不同先调整映射。
ffprobe -v error -show_format -show_streams -of json output.mp4
需要仅换容器时可考虑 -c copy,但先确认目标容器支持原编码,并使用新的输出文件;它与上面的重编码是不同方案。
录音 Ctrl-C 结束;monitor source 捕获对应输出的混音,普通 source 是输入设备。parec 默认原始流不能仅改扩展名冒充 WAV。FFmpeg 最后一条是需要相应编码器的转码示例,CRF 23 不是通用质量承诺;-c copy 不重编码但仍受目标容器兼容性约束。编码器列出不证明硬件设备、权限与驱动都可用。
Wayland 录屏优先使用桌面支持的 portal/录制工具;wf-recorder 要求合适的 screencopy 协议支持,不能推广到所有 Wayland 桌面。音频参数使用实际 source 名,不直接把“HDMI”当设备名。媒体键可绑定上面的 wpctl 命令及 playerctl play-pause/next/previous,按桌面当前配置语法接入。固件问题先查内核明确缺少的文件,再按发行版安装对应 firmware,见 内核。
WirePlumber wpctl · Linking policy · FFmpeg · wf-recorder