From 52b539cdfb8d516ce7edbb77f14373a8cec974e9 Mon Sep 17 00:00:00 2001 From: edgevoid Date: Mon, 14 Sep 2026 07:31:05 +0800 Subject: [PATCH] =?UTF-8?q?v1.3.1:=20=E4=BF=AE=E6=96=AD=E7=94=B5=E9=9A=94?= =?UTF-8?q?=E5=A4=9C=E9=87=8D=E5=90=AF=E5=90=8E"=E5=8F=88=E6=B2=A1?= =?UTF-8?q?=E5=A3=B0=E9=9F=B3"(=E7=89=A9=E7=90=86=E8=BE=93=E5=87=BA?= =?UTF-8?q?=E4=BB=8E=E5=AD=98=E6=A1=A3=E6=81=A2=E5=A4=8D=E6=88=90=E6=97=A7?= =?UTF-8?q?=E5=80=BC,=20=E6=A1=A5=E5=8F=AA=E7=9B=91=E8=A7=86=E5=8F=98?= =?UTF-8?q?=E5=8C=96=E4=B8=8D=E5=85=9C=E5=BA=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因: 菜单音量桥的约定是「虚拟声卡=音量真源, 物理输出=执行器」, 但桥只监视虚拟声卡 的变化(v != ref 才折算写物理)。断电重启后物理输出从 WirePlumber 存档恢复成旧值 (实测 0.35, 而虚拟声卡 1.0 = 小 9 dB), 虚拟声卡没变 -> 桥永远不动物理 -> 声音就小着。 而看门狗没起来(非自启), 整晚没人兜底。 - web/webui.py: 桥启动时(BRIDGE_REF is None 那次)把物理输出对齐一次 - 音频守护.py: 新增第 9 项检查「物理输出音量 ≠ 虚拟声卡」(容差 0.03; 虚拟声卡自身 <0.1 时不判 —— 那是真被调小了) -> 自动纠回, 破坏测试实测 22 秒纠回 - config/组件配置单.json: 登记「音量应与虚拟声卡一致」期望项 - web/启动.sh: 起来时顺带拉起看门狗(已在跑不重复起) —— 不违背"不自启"的习惯, 但重启后一用面板就有人守; 看门狗 setsid 脱离, 挂在 systemd --user 下 - README: 版本记录 1.3.1 --- README.md | 4 ++++ config/组件配置单.json | 5 ++++- web/webui.py | 10 +++++++++ web/启动.sh | 23 +++++++++++++++++++ 音频守护.py | 51 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 92 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b91d3a6..76106d4 100644 --- a/README.md +++ b/README.md @@ -486,6 +486,10 @@ python3 web/webui.py --port 8788 --bind 127.0.0.1 # 或直接运行后 ## 11. 版本记录 +### 1.3.1 +- 修复:**断电隔夜重启后"又没声音"** —— 物理输出的音量会从 WirePlumber 存档恢复成旧值(实测 0.35,而虚拟声卡是 1.0),而菜单音量桥只"监视虚拟声卡变化"、虚拟没变就永远不动物理 → 声音凭空小 9 dB。**两处兜底**:① 桥启动时把物理输出对齐一次;② 看门狗新增第 9 项检查「物理输出音量 ≠ 虚拟声卡」并自动纠回(实测 22 秒纠回) +- 修复:**隔夜无人看守** —— 看门狗不做开机自启(既有约定),重启后链路就没有守护。现在 `web/启动.sh` 起来时**顺带把看门狗拉起**(已在跑则不重复起);看门狗用 `setsid` 脱离会话,挂在 `systemd --user` 下,Hermes 关了它照样跑 + ### 1.3.0 - **离线渲染**(`离线渲染.py`):音频 / 视频 → 空间音频 FLAC(96 kHz 双耳)。链路与实时虚拟声卡同参数:`pan` 上混 5.1 → `sofalizer`(同一个 H4 SOFA)→ 房间 IR 卷积(`afir`)→ `loudnorm`。视频自动取音轨;已是 5.1 的片源不重上混 - **两道锁**:渲染锁(一轮任务期间**不接受新任务**,直接拒绝而非排队堆积);文件就绪检查(相隔 1.2 秒两次 stat 的大小/时间不变 + ffprobe 能读出音轨,防止把还在下载 / 写入的文件扔进渲染) diff --git a/config/组件配置单.json b/config/组件配置单.json index 55096f5..4a2b91b 100644 --- a/config/组件配置单.json +++ b/config/组件配置单.json @@ -58,7 +58,10 @@ "物理输出": { "匹配": "alsa_output.usb-EDIFIER_Technology_EDIFIER_Fit900NB_4250315939393214-00.iec958-stereo", "_匹配说明": "USB 耳机重启后 profile 可能在 iec958-stereo / analog-stereo 之间变, 名字会变; 所以按 EDIFIER 前缀匹配即可", - "_路由": "数字(S/PDIF) 优先, 96kHz/24bit(S24LE); 实测音质优于模拟" + "_路由": "数字(S/PDIF) 优先, 96kHz/24bit(S24LE); 实测音质优于模拟", + "音量应与虚拟声卡一致": true, + "容差": 0.03, + "_音量说明": "★2026-09-14 教训: 断电隔夜重启后本级会从 WirePlumber 存档恢复成旧值(实测 0.35, 而虚拟声卡是 1.0), 链上每一项都对却声音凭空小 9 dB。约定 = 虚拟声卡是**音量真源**、这里是**执行器**, 两者应一致; 虚拟声卡自身 <0.1 时不判(那是真被调小了, 别乱纠)" }, "上混链": { diff --git a/web/webui.py b/web/webui.py index e6f23c2..d0da338 100644 --- a/web/webui.py +++ b/web/webui.py @@ -722,6 +722,16 @@ def vol_bridge_tick() -> str | None: # ★ 不能采信 VOL_FILE(存档): 重建/别处改动之后实况可能与存档不一致, # 那桥会把这个差值当成"菜单音量"折算一次 → 音量无故跳一下(2026-09-13 教训)。 BRIDGE_REF = v if v > 0.01 else 0.35 + # ★★ 启动时顺手把**物理输出对齐一次**(2026-09-14 教训): 断电隔夜重启后物理输出会 + # 从 WirePlumber 存档恢复成旧值(实测 0.35 而虚拟声卡是 1.0) —— 桥只"监视变化", + # 虚拟没变它就永远不动物理, 于是整晚/整天的声音都小 9 dB。启动时对齐一次即可。 + p0, pm0 = vol_of(BRIDGE_PID, timeout=4) + if pm0: + sh("wpctl", "set-mute", str(BRIDGE_PID), "0", timeout=4) + return "启动同步: 物理输出原本静音 → 已解除" + if p0 >= 0.0 and abs(p0 - BRIDGE_REF) > 0.02: + msg = set_speaker(BRIDGE_REF) + return "启动同步: 物理输出 %.3f → %.3f (对齐虚拟声卡) — %s" % (p0, BRIDGE_REF, msg) return None ref = BRIDGE_REF p, _pm = vol_of(BRIDGE_PID, timeout=4) diff --git a/web/启动.sh b/web/启动.sh index ae15320..06ca7e4 100755 --- a/web/启动.sh +++ b/web/启动.sh @@ -16,3 +16,26 @@ else cat /tmp/cinema-webui.log exit 1 fi + +# ---------------------------------------------------------------- 看门狗 +# ★ 2026-09-14: 隔夜断电重启后"又没声音"的真因之一 = 看门狗没起来(它刻意不做开机自启, +# 这是老板的习惯), 于是重启后没人看着链路(那次是物理输出被恢复成旧值 0.35)。 +# 面板是日常入口, 由它顺手把看门狗拉起最自然 —— 不算"自启", 但重启后一用面板就有人守。 +# 判在不在跑一律读 /proc//cmdline: grep -f 会匹配到自己的命令行把自己算进去。 +n=0 +for d in /proc/[0-9]*; do + [ -r "$d/cmdline" ] || continue + if tr '\0' ' ' < "$d/cmdline" 2>/dev/null | grep -q '音频守护\.py'; then + n=$((n + 1)) + fi +done +if [ "$n" -eq 0 ]; then + GD="$(dirname "$(readlink -f "$0")")/.." + mkdir -p "$HOME/.local/state/cinema-spatial" + ( cd "$GD" && setsid -f python3 音频守护.py \ + > "$HOME/.local/state/cinema-spatial/guardian-console.log" 2>&1 ) + sleep 1 + echo "看门狗: 已拉起(30 秒一轮, 扫到问题自动修)" +else + echo "看门狗: 已在跑($n 个)" +fi diff --git a/音频守护.py b/音频守护.py index b7ac06a..489b41e 100644 --- a/音频守护.py +++ b/音频守护.py @@ -334,6 +334,23 @@ def check_all(cfg: dict[str, Any]) -> list[str]: if not panel_ok(): probs.append("🟢 面板 8788 无响应") + # 9) 物理输出音量 vs 虚拟声卡 —— 2026-09-14 教训: 断电重启后物理输出会从 + # WirePlumber 存档恢复成旧值(实测 0.35), 而虚拟声卡是 1.0, 链上全对 + # 却声音凭空小 9 dB。本项目约定「虚拟声卡 = 音量真源, 物理输出 = 执行器」, + # 两者该一致。虚拟声卡自己很低(<0.1)时不判 —— 那是真被调小了, 别乱纠。 + vs = node_ids(want_sink) + pn = phys_name() + if vs and pn: + vv, vm = vol_of(vs[0]) + for pid in node_ids(pn): + pv, pm = vol_of(pid) + if pm: + probs.append("🔴 物理输出 %s 被静音 —— 链上全对却没声音" % pn) + elif not vm and vv > 0.1 and pv >= 0.0 and abs(pv - vv) > 0.03: + probs.append("🟡 物理输出音量 %.3f ≠ 虚拟声卡 %.3f " + "—— 多半是重启后从存档恢复的旧值, 声音会小一大截" % (pv, vv)) + break + return probs @@ -385,6 +402,8 @@ def _pick_fix(desc: str) -> tuple[str, str]: return ("dup", "重建链路(生成器会清理同名 conf)") if "应为 1.000" in desc: return ("unitgain", "把中间级音量纠回 1.000") + if "物理输出" in desc: + return ("physvol", "把物理输出对齐到虚拟声卡(音量真源)") if "面板" in desc: return ("panel", "拉起面板") return ("misc", "重建链路") @@ -418,9 +437,41 @@ def _do_fix(key: str, cfg: dict[str, Any]) -> str: return "; ".join(done) if done else "无需纠正" if key == "panel": return fix_panel() + if key == "physvol": + return fix_phys_volume(cfg) return fix_rebuild() +def fix_phys_volume(cfg: dict[str, Any]) -> str: + """把物理输出对齐到虚拟声卡。 + + 本项目的音量真源是**虚拟声卡**(面板滑块、菜单媒体键、系统设置都写它), 物理输出 + 只是执行器。断电重启后物理输出可能从 WirePlumber 存档恢复成旧值(2026-09-14 实测 + 0.35 vs 虚拟声卡 1.0), 于是链上每一项都对却声音小 9 dB —— 就是这一项在兜底。 + """ + want = str((cfg.get("默认输出") or {}).get("必须是", "cinema_spatial_up_sink")) + vs = node_ids(want) + if not vs: + return "! 找不到虚拟声卡 %s" % want + vv, vm = vol_of(vs[0]) + if vm or vv <= 0.01: + return "! 虚拟声卡本身就不对(音量 %.3f, 静音=%s), 该先修它" % (vv, vm) + pn = phys_name() + if not pn: + return "! 找不到物理输出" + pids = node_ids(pn) + if not pids: + return "! 物理输出节点不存在" + pid = pids[0] + before, bm = vol_of(pid) + sh("wpctl", "set-mute", str(pid), "0") + sh("wpctl", "set-volume", str(pid), "%.3f" % vv) + after, am = vol_of(pid) + if am or abs(after - vv) > 0.03: + return "! 物理输出写不进去(仍 %.3f, 目标 %.3f)" % (after, vv) + return "物理输出 %.3f%s -> %.3f (对齐虚拟声卡)" % (before, "(原本静音)" if bm else "", after) + + def main() -> None: ap = argparse.ArgumentParser(description="电影院空间音频看门狗") ap.add_argument("--once", action="store_true", help="只跑一轮")