From d331128e88a99f2379e5110108ba0dd788371c76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=B3=96=E7=B3=96?= Date: Thu, 24 Sep 2026 09:36:36 +0800 Subject: [PATCH] =?UTF-8?q?=E8=BE=93=E5=87=BA=E8=AE=BE=E5=A4=87=E4=B8=8D?= =?UTF-8?q?=E5=86=99=E6=AD=BB:=20=E9=9D=A2=E6=9D=BF=E8=AE=BE=E5=A4=87?= =?UTF-8?q?=E9=80=89=E6=8B=A9=20+=20=E8=87=AA=E5=8A=A8=E6=8C=82=E8=BD=BD?= =?UTF-8?q?=E5=AE=88=E6=8A=A4(=E6=B2=BB=E5=BC=80=E6=9C=BA=E5=90=8E"?= =?UTF-8?q?=E5=8F=8C=E9=87=8D=E6=B7=B7=E5=93=8D")?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 问题: 30-collaplex-eq.conf 的 node.target 是安装时探测一次写死的。开机时设备若还没枚举 (USB 耳机晚到很常见: 09-24 实测开机 08:57、耳机 09:19:43 才出现; 09-23 是 21:01 → 21:03), 建链时目标不存在 → 末段连到别处或干脆不连, 设备出现后不会自己改回来 → 听感"双重混响", 只能 stop + start - 新增 web/output_route.py: 设备清单(pw-dump) + 选择落盘(output.json) + 守护线程(每 3 秒核对 collaplex_eq_out 只连选中设备, 不对就拆错的、按声道补对的) → 设备一出现自动挂上 - web/server.py: GET /api/devices, POST /api/device(落盘 + 立刻挂载), 快照带 output 段, main() 起守护线程 - web/index.html: 「输出设备」卡片(下拉 + 应用 + 状态 + 守护备注; 设备表变化才重建选项, 免得 60 Hz 重建打断点击) - 脚本/安装.sh: 优先用记住的设备(不在位也照样按它建链), 不再"猜第一个 digital" - 实测: 面板真点应用切走再切回 ✓ / 连错 2 秒自动纠回 ✓ / 全拆 3 秒自动挂回 ✓ / start 自检 打印 [在位] ✓ / 面板像素验收 ✓; pyright strict 0 errors 0 warnings - 坑: pw-dump 的 Link 键名是 output-port-id / input-port-id(写 output-port 一条连线都读不到); 面板服务 enable --now 不会重启已在运行的实例(改 web/*.py 必须 restart) --- .gitignore | 3 + README.md | 45 +++++ pipewire/30-collaplex-eq.conf | 5 + web/index.html | 51 ++++++ web/output_route.py | 299 ++++++++++++++++++++++++++++++++++ web/server.py | 23 ++- 测试/包络示波器.py | 134 +++++++++++++++ 测试/面板截图.py | 57 +++++++ 脚本/安装.sh | 55 +++++-- 9 files changed, 657 insertions(+), 15 deletions(-) create mode 100644 web/output_route.py create mode 100644 测试/包络示波器.py create mode 100644 测试/面板截图.py 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, 每帧重建