v1.3.1: 修断电隔夜重启后"又没声音"(物理输出从存档恢复成旧值, 桥只监视变化不兜底)

根因: 菜单音量桥的约定是「虚拟声卡=音量真源, 物理输出=执行器」, 但桥只监视虚拟声卡
的变化(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
This commit is contained in:
edgevoid
2026-09-14 07:31:05 +08:00
parent d8c3a27821
commit 52b539cdfb
5 changed files with 92 additions and 1 deletions
+4
View File
@@ -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 能读出音轨,防止把还在下载 / 写入的文件扔进渲染)
+4 -1
View File
@@ -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 时不判(那是真被调小了, 别乱纠)"
},
"上混链": {
+10
View File
@@ -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)
+23
View File
@@ -16,3 +16,26 @@ else
cat /tmp/cinema-webui.log
exit 1
fi
# ---------------------------------------------------------------- 看门狗
# ★ 2026-09-14: 隔夜断电重启后"又没声音"的真因之一 = 看门狗没起来(它刻意不做开机自启,
# 这是老板的习惯), 于是重启后没人看着链路(那次是物理输出被恢复成旧值 0.35)。
# 面板是日常入口, 由它顺手把看门狗拉起最自然 —— 不算"自启", 但重启后一用面板就有人守。
# 判在不在跑一律读 /proc/<pid>/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
+51
View File
@@ -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="只跑一轮")