v1.3.2: 面板补 favicon + 修响度曲线被压; 纠正"物理输出对齐虚拟声卡"的错误假设

面板:
- 加内联 SVG favicon(深底 + 五根声波柱), 之前没有 favicon 浏览器显示默认破图标
- 响度归一化卡片: 高度 184 装不下(标题22+推子22+文字行20+间距 ≈ 70px), 而 #ln 只扣
  46px -> canvas 溢出、上沿被「目标/判据」那行文字压住。改: 卡片 210px、#ln 扣 74px、
  网格 430->456(把这 26px 补给上面两个电平表, 免得它们被压矮)。实测无重叠, #ln 126px

★ 重要纠正(2026-09-14 老板当场抓出):
- 物理输出音量是**档位**, 不是"越大越响" —— 响度由 DSP 归一化保证(实测 0.35 就很好)
- 上一版错误地把它"对齐"到虚拟声卡(1.0) -> 进削波保护、动态被压扁: 面板波形顶满
  而听感反而更小(老 bug 复现)。已撤掉三处:
  · webui.py 桥启动的"对齐一次" -> 只兜静音
  · 音频守护.py 第9项"物理≠虚拟就纠" -> 只在静音/近乎归零(<0.02)时才动手
  · fix_phys_volume() 不再写正常档位
- config/组件配置单.json: 改记「档位 0.35」+ 写明教训
This commit is contained in:
edgevoid
2026-09-14 07:44:08 +08:00
parent 52b539cdfb
commit 1e635f251b
4 changed files with 46 additions and 42 deletions
+4 -3
View File
@@ -59,9 +59,10 @@
"匹配": "alsa_output.usb-EDIFIER_Technology_EDIFIER_Fit900NB_4250315939393214-00.iec958-stereo",
"_匹配说明": "USB 耳机重启后 profile 可能在 iec958-stereo / analog-stereo 之间变, 名字会变; 所以按 EDIFIER 前缀匹配即可",
"_路由": "数字(S/PDIF) 优先, 96kHz/24bit(S24LE); 实测音质优于模拟",
"音量应与虚拟声卡一致": true,
"容差": 0.03,
"_音量说明": "★2026-09-14 教训: 断电隔夜重启后本级会从 WirePlumber 存档恢复成旧值(实测 0.35, 而虚拟声卡是 1.0), 链上每一项都对却声音凭空小 9 dB。约定 = 虚拟声卡是**音量真源**、这里是**执行器**, 两者应一致; 虚拟声卡自身 <0.1 时不判(那是真被调小了, 别乱纠)"
"档位": 0.35,
"_档位说明": "★★ 2026-09-14 修正(重要): 本级音量是一个**档位**, 实测 0.35 已经很响(响度由 DSP 归一化保证), **不是**\"越小越弱、越大越响\"。曾经错误地把它对齐到虚拟声卡(1.0) → 进削波保护、动态被压扁: 面板波形顶满而听感反而更小(老 bug 复现, 老板当场抓出)。看门狗只允许兜\"静音\"和\"近乎归零(<0.02)\", **绝不改正常档位**",
"静音应被解除": true,
"近乎归零阈值": 0.02
},
"上混链": {
+11 -4
View File
@@ -4,6 +4,11 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Collaplex 音效</title>
<!-- ★ 图标(2026-09-14): 原来没有 favicon, 浏览器标签页显示默认破图标。
用内联 SVG data URI: 深色圆角底 + 五根声波柱(与"音效"主题一致)。
data URI 里的 # 必须写成 %23; 零文件依赖, 不占额外请求。 -->
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='7' fill='%230f0f0f'/><g fill='%234ade80'><rect x='5' y='13' width='2.6' height='6' rx='1.3'/><rect x='10.5' y='9' width='2.6' height='14' rx='1.3'/><rect x='16' y='12' width='2.6' height='8' rx='1.3'/><rect x='21.5' y='9' width='2.6' height='14' rx='1.3'/><rect x='27' y='13' width='2.6' height='6' rx='1.3'/></g></svg>">
<link rel="apple-touch-icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='7' fill='%230f0f0f'/><g fill='%234ade80'><rect x='5' y='13' width='2.6' height='6' rx='1.3'/><rect x='10.5' y='9' width='2.6' height='14' rx='1.3'/><rect x='16' y='12' width='2.6' height='8' rx='1.3'/><rect x='21.5' y='9' width='2.6' height='14' rx='1.3'/><rect x='27' y='13' width='2.6' height='6' rx='1.3'/></g></svg>">
<style>
:root{
--bg:#0f0f0f; --card:#141414; --card2:#1a1a1a; --bd:#262626; --bd2:#333;
@@ -51,7 +56,7 @@
/* ★ 2026-09-13 加"响度归一化"框(固定 150px)后, 原 260px 会把两个电平表压成
一条 10px 的缝(实测截图确认) —— 行高抬到 430, 两个电平表各得约 130px。 */
.flow{display:grid;grid-template-columns:44% 4.5% 4.5% 15% 4.5% 6% 17% 4.5%;gap:0;
align-items:stretch;height:430px;overflow:hidden;margin:0 16px 0}
align-items:stretch;height:456px;overflow:hidden;margin:0 16px 0}
.col{min-width:0;min-height:0;overflow:hidden}
.meters{display:flex;flex-direction:column;gap:10px}
.meter{flex:1;background:var(--card);border:1px solid var(--bd);border-radius:12px;
@@ -72,8 +77,10 @@
.lg i{width:9px;height:2px;border-radius:1px;display:inline-block}
/* ---------- 响度归一化: 推子 + 增益历史 ---------- */
/* 高度 150 → 184: 框里加了「目标响度」滑块那一行, 不加高会把历史曲线 canvas 挤扁 */
.lnbox{flex:0 0 auto;height:184px}
/* ★ 2026-09-14 修: 高度 184 装不下(标题 22 + 推子 22 + 文字行 20 + 间距 = ~70px),
而 #ln 只扣了 46px → canvas 溢出、上沿被「目标/判据」那行文字压住。
现在: 卡片加高到 210, 扣减改 74px(与实际占用一致), canvas 归位并为 0dB 居中。 */
.lnbox{flex:0 0 auto;height:210px}
.lntgt{margin:6px 0 0;font-size:11px}
.lntgt input[type=range]{flex:1;min-width:0}
.lnfader{margin:8px 1px 0}
@@ -87,7 +94,7 @@
border-radius:50%;background:#e5e5e5;border:2px solid #0f0f0f;
box-shadow:0 0 0 1px var(--bd);transition:left .12s linear}
.lnfoot{display:flex;justify-content:space-between;font-size:11px;color:var(--mut);margin-top:6px}
#ln{width:100%;display:block;height:calc(100% - 46px);min-height:30px;margin-top:5px}
#ln{width:100%;display:block;height:calc(100% - 74px);min-height:60px;margin-top:6px}
/* 竖排开关 */
.vsw{background:var(--card);border:1px solid var(--bd);border-radius:12px;cursor:pointer;
+6 -8
View File
@@ -722,16 +722,14 @@ 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)
# ★★ 2026-09-14 修正: **不要**在这里把物理输出"对齐"到虚拟声卡 —— 物理输出音量
# 是一个**档位**(WirePlumber 存档里多少就是多少), 响度由 DSP 归一化负责。
# 上一版就是这么把老板调好的 0.35 顶到 1.0 的, 结果进削波保护、动态被压扁:
# 面板波形顶满而听感反而更小(老 bug 复现)。启动时**只兜静音**, 档位一律不动。
_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 "启动检查: 物理输出原本静音 → 已解除(档位不动)"
return None
ref = BRIDGE_REF
p, _pm = vol_of(BRIDGE_PID, timeout=4)
+23 -25
View File
@@ -334,21 +334,19 @@ 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)
# 9) 物理输出被静音 / 近乎归零
# ★★ 2026-09-14 修正: 上一版按"物理输出应等于虚拟声卡"去纠, 结果把老板调好的 0.35
# 顶到 1.0 → 进削波保护、动态被压扁 —— 面板"波形顶满"而听感反而更小(老 bug 复现)。
# 实际约定: **物理输出音量是个档位**(存档里多少就是多少, 0.35 就很响), 响度由 DSP
# 归一化保证。所以这里只兜"静音 / 近乎归零"这种明显故障, **绝不改档位数值**。
pn = phys_name()
if vs and pn:
vv, vm = vol_of(vs[0])
if pn:
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))
elif pv >= 0.0 and pv < 0.02:
probs.append("🔴 物理输出音量 %.3f (近乎归零) —— 只剩这一级还有问题" % pv)
break
return probs
@@ -443,19 +441,14 @@ def _do_fix(key: str, cfg: dict[str, Any]) -> str:
def fix_phys_volume(cfg: dict[str, Any]) -> str:
"""把物理输出对齐到虚拟声卡
""""物理输出被静音 / 近乎归零"
本项目的音量真源是**虚拟声卡**(面板滑块、菜单媒体键、系统设置都写它), 物理输出
只是执行器。断电重启后物理输出可能从 WirePlumber 存档恢复成旧值(2026-09-14 实测
0.35 vs 虚拟声卡 1.0), 于是链上每一项都对却声音小 9 dB —— 就是这一项在兜底。
★★ 2026-09-14 修正: 本函数**不再把物理输出"对齐"到虚拟声卡** —— 那是错的假设。
物理输出音量是一个**档位**(WirePlumber 存档里多少就是多少, 实测 0.35 就很响),
响度由 DSP 归一化负责。把它顶到 1.0 会进削波保护、动态被压扁: 面板波形顶满
而听感反而更小(老板当场抓出来的老 bug 复现)。所以这里只解除静音, 且只在
"近乎归零"时把档位救到一个安全值, 正常档位一律不动。
"""
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 "! 找不到物理输出"
@@ -464,12 +457,17 @@ def fix_phys_volume(cfg: dict[str, Any]) -> str:
return "! 物理输出节点不存在"
pid = pids[0]
before, bm = vol_of(pid)
if bm:
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)
if am:
return "! 静音解不掉(仍静音)"
return "物理输出原本静音 -> 已解除(音量 %.3f 不动)" % after
if 0.0 <= before < 0.02:
sh("wpctl", "set-volume", str(pid), "0.35")
after, _am = vol_of(pid)
return "物理输出 %.3f 近乎归零 -> 救回安全档位 %.3f" % (before, after)
return "物理输出 %.3f 正常, 无需处理(档位不归看门狗管)" % before
def main() -> None: