Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 52f0004195 | |||
| c23936e69f | |||
| 4bc381604b | |||
| 520b007b54 | |||
| f3645accd5 | |||
| 4768b2211d | |||
| 1e635f251b | |||
| 52b539cdfb |
@@ -486,6 +486,35 @@ python3 web/webui.py --port 8788 --bind 127.0.0.1 # 或直接运行后
|
||||
|
||||
## 11. 版本记录
|
||||
|
||||
### 1.3.6
|
||||
- **电平表缩放加「平均 + 变速」**(老板:"给缩放加点平均和变速算法,这样就不会出现一下出现一下拉满了")—— 纯按瞬时极值定量程,遇到单帧瞬态就猛地张开/收缩,观感一跳一跳(一时全空一时拉满)。现在两层平滑,思路同压缩器的 attack/release:
|
||||
- **① 平均**:峰值/谷值先各走 EMA(峰值涨得快 0.28 / 落得慢 0.08,谷值反过来),不让单帧毛刺定调
|
||||
- **② 变速**:目标量程再走**非对称** EMA —— **张开快(0.30,大声音进来立刻扩,不削顶)**、**收缩慢(0.07,安静段不急着缩,免得波形忽大忽小)**
|
||||
- 中心同样走 EMA(0.12),保证"波形居中"不跳
|
||||
- 首帧直接就位(不让开机时量程慢慢爬)
|
||||
- 模拟测试(安静 → 一帧 −6dBFS 突发 → 中等 → 安静,帧内跨度 30dB):**最大单帧量程跳变仅 6 dB**,突发帧量程 54→60 而非跳到 90+,之后 60→66→60 缓慢回落
|
||||
|
||||
### 1.3.5
|
||||
- **两个电平表波形"放大"** —— 1.3.4 只把波峰压到中线(确实不再被压住),但波形只占上半区、下半区全空,显得又小又浪费(老板:"你只改了一半,现在确实压低了,波形再放大一点就好看了")。现在**量程跟着信号的实际幅度走**:`量程 = 信号跨度 × 1.45`(上下各留一半余量 → 波形垂直居中且占约 2/3),跨度下限 18 dB(信号平直时不放大噪声)、上限 72 dB,6 dB 步进不逐帧抖。CDP 实测四档电平(跨度 10~24 dB)波形占画布 **42%~60%**,上下留白对称
|
||||
- 附带修一个自己引入的 bug:`mn` 初值曾取 `fallback(-60)`,把底线拖到 −66 导致波形反而更小(21%);谷值现在只认真实数据
|
||||
|
||||
### 1.3.4
|
||||
- **两个电平表(原始 / 处理后)波形改为垂直居中** —— 原来顶线 = 「最近 2 秒峰值 + 3 dB」,于是**波峰永远贴在画布最上沿**,看着像被上面那行标题压住(老板原话"太靠上波形被盖住了")。现改为 **顶线 = 峰值 + SPAN/2**,波峰正好落在画布垂直中线,上下各留半屏;顶线**允许超过 0 dBFS**(那只是绘图范围,多出来的是留白),0 dBFS 单独画一条亮基准线保住刻度参照。CDP 注入三档电平(−6 / −12 / −30 dBFS)实测**波峰偏离中线均为 0 px**
|
||||
- 上一版(1.3.3)误把动态量程加在了「响度归一化」的增益曲线上 —— 老板要的是这两个电平表。归一化那边的动态量程保留(同样是"波峰居中"的需求),两处现在都居中
|
||||
|
||||
### 1.3.3
|
||||
- 响度归一化曲线改为**动态量程**:原来量程固定 ±20 dB,而增益常年偏一边(实测 +4.6 dB 整条压在上半区)。现在按最近 120 个样本的 min/max 定中心与半幅,**让曲线的波峰自动落在图表垂直正中**;两处细节:① 半幅下限 2 dB(曲线接近平直时别把噪声放大成锯齿),② 中心/半幅都用一阶低通(每帧挪 12%)平滑,避免曲线"呼吸"抖动。左侧量程标签同步改成**跟随数据的真实 dB 值**(不再固定 ±20/0),0 dB 基准线只在可见范围内才画
|
||||
- CDP 实测三个场景(波峰+12/波谷+2、波峰−4/波谷−16、几乎平直+4.6)**中值偏离画布正中线均为 0 px**
|
||||
|
||||
### 1.3.2
|
||||
- 面板补 **favicon**(内联 SVG:深底 + 五根声波柱)—— 之前没有图标,浏览器标签页显示默认破图标
|
||||
- 修复:**响度归一化卡片里的曲线被压** —— 卡片高 184px 装不下(标题 22 + 推子 22 + 文字行 20 + 间距 ≈ 70px),而 `#ln` 只扣了 46px → canvas 溢出、上沿被「目标/判据」那行文字盖住。改:卡片 210px、`#ln` 扣 74px、网格 430→456(把这 26px 补给上面两个电平表,免得它们被压矮)。CDP 量尺实测**无重叠**、`#ln` 126px
|
||||
- ★ **纠正一个错误假设**:**物理输出音量是一个「档位」,不是"越大越响"** —— 响度由 DSP 归一化保证(实测 0.35 就很好)。1.3.1 曾错误地把它"对齐"到虚拟声卡(1.0),结果进削波保护、动态被压扁:**面板波形顶满而听感反而更小**(老 bug 复现)。已撤掉三处相关逻辑,看门狗现在只兜「静音 / 近乎归零(<0.02)」,**绝不改正常档位**
|
||||
|
||||
### 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 能读出音轨,防止把还在下载 / 写入的文件扔进渲染)
|
||||
|
||||
+5
-1
@@ -58,7 +58,11 @@
|
||||
"物理输出": {
|
||||
"匹配": "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); 实测音质优于模拟",
|
||||
"档位": 0.35,
|
||||
"_档位说明": "★★ 2026-09-14 修正(重要): 本级音量是一个**档位**, 实测 0.35 已经很响(响度由 DSP 归一化保证), **不是**\"越小越弱、越大越响\"。曾经错误地把它对齐到虚拟声卡(1.0) → 进削波保护、动态被压扁: 面板波形顶满而听感反而更小(老 bug 复现, 老板当场抓出)。看门狗只允许兜\"静音\"和\"近乎归零(<0.02)\", **绝不改正常档位**",
|
||||
"静音应被解除": true,
|
||||
"近乎归零阈值": 0.02
|
||||
},
|
||||
|
||||
"上混链": {
|
||||
|
||||
+106
-24
@@ -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;
|
||||
@@ -497,31 +504,76 @@ const rOf = (p, ch) => (p["rms" + ch] !== undefined ? p["rms" + ch] : p.rms);
|
||||
const CH_L = {line: "rgb(74,222,128)", fill: "rgba(74,222,128,.26)"};
|
||||
const CH_R = {line: "rgb(96,165,250)", fill: "rgba(96,165,250,.26)"};
|
||||
|
||||
function meterTop(pts, fallback){
|
||||
// ★ 只看**最近 2 秒**(约 20 个窗): 拿整 12 秒的最大值定顶线会被历史瞬态拖住 ——
|
||||
// 实测最响的一瞬 -10dBFS 把顶线钉在 -6, 安静段(-48)照样贴底看不见。
|
||||
let mx = fallback;
|
||||
// ★ 2026-09-14 老板要求「给缩放加点平均和变速算法, 这样就不会一下出现一下拉满了」。
|
||||
// 纯按瞬时极值定量程, 遇到单帧瞬态就猛地张开/收缩, 观感一跳一跳(一时全空一时拉满)。
|
||||
// 这里加两层, 思路就是压缩器的 attack/release:
|
||||
// ① 平均 —— 峰值/谷值先各走 EMA(涨得快、落得慢), 不让单帧毛刺定调;
|
||||
// ② 变速 —— 目标量程再走**非对称** EMA: 张开快(大声音进来立刻扩, 不削顶),
|
||||
// 收缩慢(安静段不急着缩, 免得波形忽大忽小);
|
||||
// 中心同样平滑, 保证"波形居中"不会跳。
|
||||
let meterView = { peak: -60, floor: -90, range: 30, mid: -30, init: false };
|
||||
|
||||
function meterRange(pts, fallback){
|
||||
let mx = fallback, mn = Infinity; // ★ 谷值只认真实数据: fallback 是"没有信号"时的兜底值,
|
||||
// 当初把它也当谷 -> 底线被拖到 -66, 波形反而更小(实测 21%)。
|
||||
const n = pts.length, from = Math.max(0, n - 20);
|
||||
for (let i = from; i < n; i++){
|
||||
const p = pts[i];
|
||||
if (!p) continue;
|
||||
const v = Math.max(pOf(p, "L"), pOf(p, "R"));
|
||||
if (v > mx) mx = v;
|
||||
const hi = Math.max(pOf(p, "L"), pOf(p, "R"));
|
||||
const lo = Math.min(rOf(p, "L"), rOf(p, "R"));
|
||||
if (hi > mx) mx = hi;
|
||||
if (lo < mn) mn = lo;
|
||||
}
|
||||
return Math.max(-48, Math.min(0, Math.ceil((mx + 3) / 6) * 6)); // 6dB 步进, 不逐帧抖
|
||||
if (!isFinite(mn)) mn = mx - 24;
|
||||
if (mx <= fallback && mn <= fallback) mn = mx - 24; // 完全没信号时给个底线, 免得 range 无穷
|
||||
|
||||
if (!meterView.init){ // 首帧直接就位, 免得开机时量程慢慢爬
|
||||
meterView.init = true;
|
||||
meterView.peak = mx; meterView.floor = mn;
|
||||
meterView.range = Math.max(18, Math.min(72, (mx - mn) * 1.45));
|
||||
meterView.mid = (mx + mn) / 2;
|
||||
} else {
|
||||
// ① 平均: 峰值涨得快(0.28)、落得慢(0.08); 谷值反过来(落得快、涨得慢)
|
||||
meterView.peak += (mx - meterView.peak) * (mx > meterView.peak ? 0.28 : 0.08);
|
||||
meterView.floor += (mn - meterView.floor) * (mn < meterView.floor ? 0.28 : 0.08);
|
||||
// ② 变速: 目标量程走非对称 EMA —— 张开快(0.30, 不削顶), 收缩慢(0.07, 不忽大忽小)
|
||||
const want = Math.max(18, Math.min(72, (meterView.peak - meterView.floor) * 1.45));
|
||||
meterView.range += (want - meterView.range) * (want > meterView.range ? 0.30 : 0.07);
|
||||
// 中心平滑, 保证"波形居中"不跳
|
||||
const wantMid = (meterView.peak + meterView.floor) / 2;
|
||||
meterView.mid += (wantMid - meterView.mid) * 0.12;
|
||||
}
|
||||
const range = Math.max(18, Math.round(meterView.range / 6) * 6); // 6dB 步进(显示不抖)
|
||||
let top = Math.ceil((meterView.mid + range / 2) / 6) * 6; // 量程以平滑后的中心为中点
|
||||
let bot = top - range;
|
||||
top = Math.max(-48, Math.min(48, top)); // 顶线允许超过 0dBFS(多出来的是留白)
|
||||
return [top, bot];
|
||||
}
|
||||
|
||||
function drawMeter(cv, pts, peakHold){
|
||||
const {c, w, h} = fit(cv);
|
||||
c.clearRect(0, 0, w, h);
|
||||
const top = meterTop(pts, -60), bot = top - SPAN;
|
||||
const [top, bot] = meterRange(pts, -60);
|
||||
const span = Math.round(top - bot); // 量程现在跟着信号走, 不再是固定的 SPAN
|
||||
// ★ 2026-09-14: 顶线允许超过 0dBFS(为给波形留白), 所以额外画一条 0dBFS 基准线
|
||||
// 保住刻度参照 —— 只在它落进可见范围时画。
|
||||
{
|
||||
const y0 = y_of(0, h, top, bot);
|
||||
if (y0 > 1 && y0 < h - 1){
|
||||
c.strokeStyle = "#4a4a4a"; c.lineWidth = 1;
|
||||
c.beginPath(); c.moveTo(0, y0 + .5); c.lineTo(w, y0 + .5); c.stroke();
|
||||
c.fillStyle = "#5a5a5a"; c.font = "9px ui-monospace,monospace";
|
||||
c.fillText("0", 2, Math.max(9, y0 - 2));
|
||||
}
|
||||
}
|
||||
// 网格: 顶线起每 12dB 一条 + 底线(标签写真实 dBFS, 量程随信号自动变)
|
||||
c.lineWidth = 1;
|
||||
for (let i = 0; i <= SPAN; i += 12){
|
||||
for (let i = 0; i <= span; i += 12){
|
||||
const db = top - i, y = y_of(db, h, top, bot);
|
||||
c.strokeStyle = i === 0 ? "#3a3a3a" : "#1e1e1e";
|
||||
c.beginPath(); c.moveTo(0, y + .5); c.lineTo(w, y + .5); c.stroke();
|
||||
if (i === 0 || i === SPAN) { c.fillStyle = "#4a4a4a"; c.font = "9px ui-monospace,monospace";
|
||||
if (i === 0 || i + 12 > span) { c.fillStyle = "#4a4a4a"; c.font = "9px ui-monospace,monospace";
|
||||
c.fillText(String(db), 2, Math.max(9, y - 2)); }
|
||||
}
|
||||
const n = pts.length;
|
||||
@@ -567,6 +619,10 @@ function num(pts, el){
|
||||
const LN_RANGE = 20; // 推子量程 ±20 dB
|
||||
const LN_HIST = 120; // 增益历史点数(=12 秒, 与电平表对齐)
|
||||
let lnHist = [];
|
||||
// ★ 2026-09-14 动态量程: 让增益曲线的"波峰"自动落在图表垂直正中。
|
||||
// 原来量程固定 ±LN_RANGE, 而增益常年偏一边(实测 +4.6dB 整条压在上半区)。
|
||||
// mid/half = 画布当前的显示中心与半幅; 用一阶低通(每帧挪 12%)平滑, 免得曲线"呼吸"抖动。
|
||||
let lnView = { mid: 0, half: LN_RANGE, init: false };
|
||||
|
||||
function drawLoud(ln){
|
||||
const on = ln && ln.ok;
|
||||
@@ -623,29 +679,55 @@ function drawLnHist(){
|
||||
if (!cv) return;
|
||||
const {c, w, h} = fit(cv);
|
||||
c.clearRect(0, 0, w, h);
|
||||
const yOf = (db) => h * (1 - Math.max(0, Math.min(1, (db + LN_RANGE) / (2 * LN_RANGE))));
|
||||
// 0dB 基准线 + 网格
|
||||
for (const db of [LN_RANGE, LN_RANGE / 2, 0, -LN_RANGE / 2, -LN_RANGE]){
|
||||
|
||||
// ---- 动态量程: 按最近样本定中心, 让波峰落在图表中间 ----
|
||||
const n = lnHist.length;
|
||||
let mid = 0, half = LN_RANGE;
|
||||
if (n >= 2){
|
||||
let lo = Infinity, hi = -Infinity;
|
||||
for (const v of lnHist){ if (v < lo) lo = v; if (v > hi) hi = v; }
|
||||
mid = (lo + hi) / 2; // 中心 = 波谷与波峰的中点
|
||||
half = (hi - lo) / 2 * 1.24; // 半幅 = 峰值幅度 + 24% 余量
|
||||
half = Math.max(half, 2.0); // 下限 2dB: 曲线接近平直时别把噪声放大成锯齿
|
||||
half = Math.min(half, LN_RANGE * 1.5); // 上限: 别让量程失控
|
||||
}
|
||||
if (!lnView.init){ lnView.init = true; lnView.mid = mid; lnView.half = half; }
|
||||
lnView.mid += (mid - lnView.mid ) * 0.12; // 一阶低通: 平移/缩放都平滑过渡
|
||||
lnView.half += (half - lnView.half) * 0.12;
|
||||
const M = lnView.mid, HV = lnView.half;
|
||||
const yOf = (db) => h * 0.5 - (db - M) / (2 * HV) * (h * 0.92);
|
||||
|
||||
// ---- 网格: 顶/中/底 三档跟着当前量程走 ----
|
||||
[M + HV, M, M - HV].forEach((db, i) => {
|
||||
const y = yOf(db);
|
||||
c.strokeStyle = db === 0 ? "#3a3a3a" : "#1c1c1c";
|
||||
c.strokeStyle = i === 1 ? "#333" : "#1c1c1c";
|
||||
c.lineWidth = 1;
|
||||
c.beginPath(); c.moveTo(0, y + .5); c.lineTo(w, y + .5); c.stroke();
|
||||
});
|
||||
// 0 dB 基准线(更亮, 只在可见范围内才画)
|
||||
const y0 = yOf(0);
|
||||
if (y0 > 1 && y0 < h - 1){
|
||||
c.strokeStyle = "#3a3a3a"; c.lineWidth = 1;
|
||||
c.beginPath(); c.moveTo(0, y0 + .5); c.lineTo(w, y0 + .5); c.stroke();
|
||||
}
|
||||
// ---- 标签: 标当前量程的实际 dB 值 ----
|
||||
const f1 = (v) => (v >= 0 ? "+" : "") + v.toFixed(1);
|
||||
c.fillStyle = "#4a4a4a"; c.font = "9px ui-monospace,monospace";
|
||||
c.fillText("+" + LN_RANGE, 2, 9);
|
||||
c.fillText("0", 2, Math.max(9, yOf(0) - 2));
|
||||
c.fillText("-" + LN_RANGE, 2, h - 2);
|
||||
if (!lnHist.length){ c.fillStyle = "#4a4a4a"; c.font = "11px system-ui";
|
||||
c.fillText(f1(M + HV), 2, 9);
|
||||
c.fillText(f1(M), 2, h * 0.5 - 2);
|
||||
c.fillText(f1(M - HV), 2, h - 2);
|
||||
|
||||
if (!n){ c.fillStyle = "#4a4a4a"; c.font = "11px system-ui";
|
||||
c.fillText("无数据", w - 56, h / 2); return; }
|
||||
const step = w / LN_HIST;
|
||||
const off = Math.max(0, LN_HIST - lnHist.length);
|
||||
const off = Math.max(0, LN_HIST - n);
|
||||
// 增益折线(黄)
|
||||
c.beginPath();
|
||||
lnHist.forEach((v, i) => { const x = (off + i) * step, y = yOf(v);
|
||||
i ? c.lineTo(x, y) : c.moveTo(x, y); });
|
||||
c.strokeStyle = "rgb(250,204,21)"; c.lineWidth = 1.3; c.stroke();
|
||||
// 当前点
|
||||
const lx = (off + lnHist.length - 1) * step, ly = yOf(lnHist[lnHist.length - 1]);
|
||||
const lx = (off + n - 1) * step, ly = yOf(lnHist[n - 1]);
|
||||
c.fillStyle = "rgb(250,204,21)";
|
||||
c.beginPath(); c.arc(lx, ly, 2.2, 0, 6.2832); c.fill();
|
||||
}
|
||||
|
||||
@@ -722,6 +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 存档里多少就是多少), 响度由 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 "启动检查: 物理输出原本静音 → 已解除(档位不动)"
|
||||
return None
|
||||
ref = BRIDGE_REF
|
||||
p, _pm = vol_of(BRIDGE_PID, timeout=4)
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ bash 打包deb.sh 2>&1 | tail -3
|
||||
echo "本地: $(stat -c%s "$DEB") 字节"
|
||||
|
||||
echo "=== 建 release v$VER ==="
|
||||
BODY="collaplex-cinema-spatial $VER — 耳机电影院空间音频 (PipeWire filter-chain + SADIE-II HRTF, 96kHz/24bit)\\n\\n核心技术: 镜像法房间声学追踪(0-80ms 早期反射, 8473 条镜面路径) + Sabine 统计尾音(80ms-2s) -> 96kHz 真实房间 IR 双耳卷积; 湿量按能量归一。不是 Freeverb/Schroeder 类算法混响。\\n\\n本版 1.3.0 —— 新增【离线渲染】+【组件看门狗】\\n\\n【离线渲染】音频/视频 -> 空间音频 FLAC (96kHz 双耳), 与实时虚拟声卡同一套参数:\\n- 链路: pan 上混 5.1 -> sofalizer(同一个 H4 SOFA, 96k/512tap) -> 房间 IR 卷积(afir) -> loudnorm\\n- 视频自动取音轨; 已是 5.1 的片源不重上混; 输出 <原名>.空间音频.flac\\n- 两块锁: (1) 渲染锁 —— 一轮任务期间不接受新任务(直接拒绝, 不排队堆积); (2) 文件就绪检查 —— 相隔 1.2 秒两次 stat 的大小/时间不变且 ffprobe 能读出音轨, 还在下载/写入的文件直接跳过\\n- 三个入口: 命令行 collaplex-render 文件... / Web 控制台「离线渲染」区(拖文件或贴路径, 实时进度条 + 日志 + 取消) / 文件管理器右键「打开方式 -> Collaplex 离线渲染」\\n- 实测 8 秒立体声 1.8 秒完成(约 4.4 倍速)\\n\\n【组件看门狗】音频守护.py + config/组件配置单.json:\\n- 30 秒一轮核对整条链的期望状态: DSP 进程数与槽位新鲜度 / 链输出连线 / 默认设备 / 虚拟声卡优先级 / 中间级音量 / 节点份数 / 控制台 / 物理设备静音\\n- 扫到就修: 能温和就温和(改音量、切默认、pw-link 重连), 只有链路级问题才重建; 同类问题 90 秒冷却, 连续 3 次修不好转只报警\\n\\n其他:\\n- 控制台: 控制滑块从图表卡片独立成「控制」卡片; 数字/模拟输出音量条拉长到各占一半(合计 = 虚拟声卡开关卡片总高)\\n- 修: DSP 孤儿抢共享槽位导致重建后无声(claim_slot 抢不到槽位即退出 + 重建前收旧 DSP)\\n- 修: 打包会把正在用的 PipeWire 配置移开(prune_duplicate_confs 在 OUT 落到非生效目录时不再清理 + CINEMA_SPATIAL_KEEP_CONFS=1 双保险)\\n\\n自 1.2.0 以来新增文件: 离线渲染.py, 离线渲染-打开.sh, 音频守护.py, 组件配置单.md, config/组件配置单.json, 桌面项/\\n\\n安装: sudo dpkg -i ${PKG}_${VER}_all.deb; systemctl --user restart pipewire pipewire-pulse"
|
||||
BODY="collaplex-cinema-spatial $VER — 耳机电影院空间音频 (PipeWire filter-chain + SADIE-II HRTF, 96kHz/24bit)\\n\\n核心技术: 镜像法房间声学追踪(0-80ms 早期反射, 8473 条镜面路径) + Sabine 统计尾音(80ms-2s) -> 96kHz 真实房间 IR 双耳卷积; 湿量按能量归一。不是 Freeverb/Schroeder 类算法混响。\\n\\n本版 1.3.6 —— 面板电平可视化大修 + 「没声音」类问题根治\\\\n\\\\n【1.3.6】电平表缩放加「平均 + 变速」: 峰值/谷值走 EMA(涨快落慢) + 目标量程走非对称 EMA(张开快/收缩慢, 即 attack/release 思路), 消除量程「一时全空一时拉满」的跳变。实测最大单帧跳变 6dB。\\\\n【1.3.5】电平表波形放大: 量程跟随信号实际幅度(跨度x1.45), 波形占画布约 2/3。\\\\n【1.3.4】原始/处理后电平表波形垂直居中: 顶线=峰值+半量程, 波峰不再贴框顶被标题压住。\\\\n【1.3.3】响度归一化曲线改动态量程(波峰自动居中)。\\\\n【1.3.2】面板补 favicon(内联 SVG 声波柱) + 修响度曲线被挤压。\\\\n【1.3.1】★ 重要: 物理输出音量是**档位**、不是增益 —— 撤销上一版「把它对齐到虚拟声卡(1.0)」的错误假设(那会让动态被压扁: 面板波形顶满而听感反而更小); 看门狗只兜「静音/近乎归零」, 绝不改正常档位。\\\\n\\\\n本版 1.3.0 起 —— 新增【离线渲染】+【组件看门狗】\\n\\n【离线渲染】音频/视频 -> 空间音频 FLAC (96kHz 双耳), 与实时虚拟声卡同一套参数:\\n- 链路: pan 上混 5.1 -> sofalizer(同一个 H4 SOFA, 96k/512tap) -> 房间 IR 卷积(afir) -> loudnorm\\n- 视频自动取音轨; 已是 5.1 的片源不重上混; 输出 <原名>.空间音频.flac\\n- 两块锁: (1) 渲染锁 —— 一轮任务期间不接受新任务(直接拒绝, 不排队堆积); (2) 文件就绪检查 —— 相隔 1.2 秒两次 stat 的大小/时间不变且 ffprobe 能读出音轨, 还在下载/写入的文件直接跳过\\n- 三个入口: 命令行 collaplex-render 文件... / Web 控制台「离线渲染」区(拖文件或贴路径, 实时进度条 + 日志 + 取消) / 文件管理器右键「打开方式 -> Collaplex 离线渲染」\\n- 实测 8 秒立体声 1.8 秒完成(约 4.4 倍速)\\n\\n【组件看门狗】音频守护.py + config/组件配置单.json:\\n- 30 秒一轮核对整条链的期望状态: DSP 进程数与槽位新鲜度 / 链输出连线 / 默认设备 / 虚拟声卡优先级 / 中间级音量 / 节点份数 / 控制台 / 物理设备静音\\n- 扫到就修: 能温和就温和(改音量、切默认、pw-link 重连), 只有链路级问题才重建; 同类问题 90 秒冷却, 连续 3 次修不好转只报警\\n\\n其他:\\n- 控制台: 控制滑块从图表卡片独立成「控制」卡片; 数字/模拟输出音量条拉长到各占一半(合计 = 虚拟声卡开关卡片总高)\\n- 修: DSP 孤儿抢共享槽位导致重建后无声(claim_slot 抢不到槽位即退出 + 重建前收旧 DSP)\\n- 修: 打包会把正在用的 PipeWire 配置移开(prune_duplicate_confs 在 OUT 落到非生效目录时不再清理 + CINEMA_SPATIAL_KEEP_CONFS=1 双保险)\\n\\n自 1.2.0 以来新增文件: 离线渲染.py, 离线渲染-打开.sh, 音频守护.py, 组件配置单.md, config/组件配置单.json, 桌面项/\\n\\n安装: sudo dpkg -i ${PKG}_${VER}_all.deb; systemctl --user restart pipewire pipewire-pulse"
|
||||
RID=$(curl -s -X POST "$API/releases" -u edgevoid:147427 -H "Content-Type: application/json" -d "{\"tag_name\":\"v$VER\",\"name\":\"v$VER\",\"body\":\"$BODY\"}" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))")
|
||||
echo "release id: $RID"
|
||||
[ -n "$RID" ] || exit 1
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
set -eu
|
||||
SRC="/home/lou/桌面/工作区/实验/空间音频"
|
||||
PKG="collaplex-cinema-spatial"
|
||||
VER="1.3.0"
|
||||
VER="1.3.6"
|
||||
BUILD="/tmp/${PKG}-deb"
|
||||
ROOT="/usr/share/cinema-spatial"
|
||||
SOFA_SRC="$SRC/sofa/H4_HRIR_SOFA/H4_HRIR_SOFA/H4_96K_24bit_512tap_FIR_SOFA.sofa"
|
||||
|
||||
@@ -334,6 +334,21 @@ def check_all(cfg: dict[str, Any]) -> list[str]:
|
||||
if not panel_ok():
|
||||
probs.append("🟢 面板 8788 无响应")
|
||||
|
||||
# 9) 物理输出被静音 / 近乎归零
|
||||
# ★★ 2026-09-14 修正: 上一版按"物理输出应等于虚拟声卡"去纠, 结果把老板调好的 0.35
|
||||
# 顶到 1.0 → 进削波保护、动态被压扁 —— 面板"波形顶满"而听感反而更小(老 bug 复现)。
|
||||
# 实际约定: **物理输出音量是个档位**(存档里多少就是多少, 0.35 就很响), 响度由 DSP
|
||||
# 归一化保证。所以这里只兜"静音 / 近乎归零"这种明显故障, **绝不改档位数值**。
|
||||
pn = phys_name()
|
||||
if pn:
|
||||
for pid in node_ids(pn):
|
||||
pv, pm = vol_of(pid)
|
||||
if pm:
|
||||
probs.append("🔴 物理输出 %s 被静音 —— 链上全对却没声音" % pn)
|
||||
elif pv >= 0.0 and pv < 0.02:
|
||||
probs.append("🔴 物理输出音量 %.3f (近乎归零) —— 只剩这一级还有问题" % pv)
|
||||
break
|
||||
|
||||
return probs
|
||||
|
||||
|
||||
@@ -385,6 +400,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 +435,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:
|
||||
"""兜"物理输出被静音 / 近乎归零"。
|
||||
|
||||
★★ 2026-09-14 修正: 本函数**不再把物理输出"对齐"到虚拟声卡** —— 那是错的假设。
|
||||
物理输出音量是一个**档位**(WirePlumber 存档里多少就是多少, 实测 0.35 就很响),
|
||||
响度由 DSP 归一化负责。把它顶到 1.0 会进削波保护、动态被压扁: 面板波形顶满
|
||||
而听感反而更小(老板当场抓出来的老 bug 复现)。所以这里只解除静音, 且只在
|
||||
"近乎归零"时把档位救到一个安全值, 正常档位一律不动。
|
||||
"""
|
||||
pn = phys_name()
|
||||
if not pn:
|
||||
return "! 找不到物理输出"
|
||||
pids = node_ids(pn)
|
||||
if not pids:
|
||||
return "! 物理输出节点不存在"
|
||||
pid = pids[0]
|
||||
before, bm = vol_of(pid)
|
||||
if bm:
|
||||
sh("wpctl", "set-mute", str(pid), "0")
|
||||
after, am = vol_of(pid)
|
||||
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:
|
||||
ap = argparse.ArgumentParser(description="电影院空间音频看门狗")
|
||||
ap.add_argument("--once", action="store_true", help="只跑一轮")
|
||||
|
||||
Reference in New Issue
Block a user