diff --git a/.gitignore b/.gitignore
index be6f7e0..a4b0e6d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,3 +14,6 @@ dist/
# 本地状态与日志
*.log
+
+# 测试截图
+测试/*.png
diff --git a/README.md b/README.md
index 1f5b0d9..ff33bc3 100644
--- a/README.md
+++ b/README.md
@@ -89,6 +89,7 @@ collaplex-audio stop # 连链路一起
| **电平归一化** | 响度目标滑块 + **输入电平条** + **动态增益推子**(0 dB 在 60% 刻度线上, 量程 −18~+12 dB) |
| **混响湿度** | 0 ~ 1, 实时生效 |
| **总音量** | ±12 dB, 实时生效 |
+| **输出设备** | 下拉选输出到哪台硬件设备(HDMI / 内建模拟 / USB 耳机 …) + 「应用」; 右侧显示是否已挂载 |
| **32 段 EQ** | 1/3 倍频程(20 Hz ~ 25 kHz), 每段 ±15 dB,**10 个预设** |
- 所有滑块: **滚轮调节 · Shift+滚轮微调 · 双击归零**
@@ -270,6 +271,49 @@ collaplex-audio stop # 连链路一起
**注意**: 落盘是"最后一次改动的快照", 不是"出厂默认"。要回到出厂默认就删掉 `params.json`
并清掉共享内存(`rm /dev/shm/collaplex-eq*`), 再重启链路。
+## 输出设备选择与自动挂载(2026-09-24)
+
+**问题**: 链路末段的输出设备原来**写死**在 `30-collaplex-eq.conf` 的 `node.target`(安装时探测
+"第一个 iec958/digital")。开机时那台设备要是还没枚举出来, 会话管理器建链时目标不存在,
+末段就连到别处或者干脆不连; 设备出现了也**不会**自己改回来。听感上就是"双重混响 / 声音
+不对", 只有 `collaplex-audio stop` + `start` 才恢复。
+
+**实测的时序证据**(两次都是同一个模式 —— 链路先建好、设备后到):
+
+| 日期 | 开机 | 输出设备(EDIFIER USB)枚举 | 备注 |
+|---|---|---|---|
+| 2026-09-23 | 21:01:17 | **21:03:01**(+1 分 44 秒) | 那次 DSP 进程号也对得上: 归一化/湿量在 3997~4034, EQ 到 8924 才起 |
+| 2026-09-24 | 08:57:07 | **09:19:43**(+22 分 36 秒) | `dmesg`: `usb 3-2.2: Product: EDIFIER Fit900NB` |
+
+**现在两件事一起做**:
+
+| 机制 | 说明 |
+|---|---|
+| **面板按钮** | 「输出设备」卡片: 下拉选硬件输出 + 「应用」→ 立刻重挂 + 落盘 |
+| **选择落盘** | `~/.local/state/cinema-spatial/output.json`: `{"node_name": "...", "description": "..."}`; `脚本/安装.sh` 优先用它, 不再"猜第一个数字输出" |
+| **自动挂载守护** | 面板进程里的守护线程(`web/output_route.py`), 每 3 秒核对"选中的设备在位 且 `collaplex_eq_out` **只**连到它"; 不对就拆掉错的、补上对的 —— **设备一插上就自动挂上, 不用重启链路** |
+
+**为什么设备名可以记住、挂载不能写死**: ALSA 节点名带 USB 序列号
+(`alsa_output.usb-EDIFIER_..._4250315939393214-00.iec958-stereo`), 跨重插/重开机稳定,
+所以"选哪台"能记住; 不稳定的是**建链那一刻设备在不在**, 那正是守护要兜的事。
+
+**实测**(2026-09-24, 全部真跑):
+
+| 场景 | 结果 |
+|---|---|
+| 面板真点「应用」切到内建模拟输出 | `routed` 立刻变成模拟输出 ✓ |
+| 再选回耳机点「应用」 | `routed` 变回 EDIFIER, 提示行 `已挂到 EDIFIER Fit900NB 数字立体声(IEC958)` ✓ |
+| 手动把末段连到错的设备, 等守护 | **2 秒**自动纠回 ✓ |
+| 把末段连线全拆掉, 等守护 | **3 秒**自动挂回 ✓ |
+| `collaplex-audio start` 自检 | 打印 `输出设备: … (EDIFIER Fit900NB 数字立体声(IEC958)) [在位]` ✓ |
+
+**接口**: `GET /api/devices`(设备清单 + 当前挂载 + 守护备注)、
+`POST /api/device {"name": "alsa_output.xxx"}`(落盘 + 立刻挂载)。
+
+**注意**: 面板服务单元是 `enable --now` —— 已经在跑时 `--now` **不会**重启, 所以改完
+`web/*.py` 必须 `systemctl --user restart collaplex-audio-panel.service`(否则新代码只落在
+磁盘上, 内存里还是旧的: 实测 `GET /api/devices` 会 404)。
+
## 实测(2026-09-19)
| 指标 | 数据 |
@@ -435,6 +479,7 @@ uv run --with websocket-client python 测试/cdp_拖动验证.py # CDP 真鼠
| `pipewire/10,20,30-*.conf` | 虚拟声卡+归一化 / HRTF+混响+湿量级 / EQ |
| `pipewire/91-collaplex-clock.conf` | 强制 96 kHz(不然 IR 会被按 48k 降采样) |
| `web/server.py` `web/index.html` | 控制面板(纯标准库 + 单页, SSE 推送) |
+| `web/output_route.py` | 输出设备选择(落盘)+ 自动挂载守护(设备晚到时把末段挂上去) |
| `hrir/` `reverb/` | HRIR(12 条)与房间 IR |
| `脚本/安装.sh` `脚本/面板.sh` | 部署 / 起面板 |
| `打包deb.sh` | 打 deb 到 `发布/` |
diff --git a/pipewire/30-collaplex-eq.conf b/pipewire/30-collaplex-eq.conf
index b86dd69..45ad1b2 100644
--- a/pipewire/30-collaplex-eq.conf
+++ b/pipewire/30-collaplex-eq.conf
@@ -1,5 +1,10 @@
# Collaplex 32 段 EQ + 总音量 —— 本文件由 脚本/安装.sh 生成(占位符 __EQ_WRAPPER__ / __TARGET__)
# 链路: ... -> HRTF 核心 -> [本图] 32 段 EQ + 总音量(pipe DSP) -> 数字输出
+#
+# ★ __TARGET__ 是**用户选定的输出设备**(落盘在 ~/.local/state/cinema-spatial/output.json,
+# 面板「输出设备」卡片可改; 没选过则由安装脚本探测第一个数字输出)。
+# 设备名跨重插是稳定的(含 USB 序列号), 所以设备晚到也不改这一行 —— 由面板守护
+# (web/output_route.py)在设备出现后自动把它挂上去。
context.modules = [
{ name = libpipewire-module-filter-chain
flags = [ nofail ]
diff --git a/web/index.html b/web/index.html
index fbf2667..eaa90d7 100644
--- a/web/index.html
+++ b/web/index.html
@@ -80,6 +80,9 @@ input[type=range]::-moz-range-thumb{width:14px;height:14px;border:none;border-ra
button{background:var(--card-hi);color:var(--fg);border:1px solid var(--bd-hi);border-radius:6px;
padding:5px 12px;font:inherit;font-size:11px;cursor:pointer}
button:hover{border-color:var(--ok);color:var(--ok)}
+select{background:var(--card-hi);color:var(--fg);border:1px solid var(--bd-hi);border-radius:6px;
+ padding:5px 8px;font:inherit;font-size:11px;flex:1;min-width:0;cursor:pointer}
+select:hover{border-color:var(--ok)}
@@ -128,6 +131,17 @@ button:hover{border-color:var(--ok);color:var(--ok)}
+
+
输出设备
+
+ 输出到
+
+
+ —
+
+
换设备: 选一下再点「应用」。开机时设备晚到(USB 耳机常见)会由守护自动挂上, 不用重启链路。
+
+
32 段 EQ(1/3 倍频程 · ±15 dB)
@@ -437,8 +451,45 @@ function applyState(d) {
if (!isBusy("loud") && d.loudness) { loud.value = d.loudness.target_db; el("loud-num").textContent = (+d.loudness.target_db).toFixed(1) + " dB"; }
updateMeters(d.levels);
updateLoud(d.loudness_levels);
+ updateOutput(d.output);
state = d;
}
+
+/* ---------- 输出设备 ---------- */
+/* 设备表只在变化时重建选项: SSE 是 60 Hz, 每帧重建