前端加 4 个电平表(入L/出L/入R/出R, 双声道由 flock 分槽) + 混响湿度滑块(实时); 湿量抽成独立 DSP 增益级(wet_gain.py), 混响仍用 PipeWire convolver 跑 3 秒长 IR; 实测湿量 0 无尾巴 / 1 有 3 秒衰减尾巴
This commit is contained in:
+195
-210
@@ -7,244 +7,229 @@
|
||||
<style>
|
||||
:root{
|
||||
--bg:#0f0f0f; --card:#161616; --card-hi:#1c1c1c; --bd:#262626; --bd-hi:#343434;
|
||||
--fg:#e8e8e8; --mut:#8a8a8a; --dim:#666; --ok:#4ade80; --ok-bg:#12211a; --ok-bd:#166534;
|
||||
--r:12px;
|
||||
--fg:#e8e8e8; --mut:#8a8a8a; --dim:#666; --ok:#4ade80; --warn:#fbbf24; --hot:#f87171;
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
html,body{margin:0;background:var(--bg);color:var(--fg);
|
||||
font-family:"JetBrains Mono",ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px}
|
||||
.wrap{max-width:1280px;margin:0 auto;padding:18px 20px 40px}
|
||||
header{display:flex;align-items:baseline;gap:16px;border-bottom:1px solid var(--bd);padding-bottom:12px}
|
||||
h1{font-size:15px;font-weight:600;margin:0;letter-spacing:.02em}
|
||||
.hint{color:var(--dim);font-size:11px}
|
||||
.meter{margin-left:auto;display:flex;gap:18px;align-items:baseline;color:var(--mut);font-size:11px}
|
||||
.meter b{color:var(--ok);font-weight:500}
|
||||
.rows{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin:16px 0}
|
||||
.card{background:var(--card);border:1px solid var(--bd);border-radius:var(--r);padding:14px 16px}
|
||||
.card h2{margin:0 0 12px;font-size:12px;font-weight:500;color:var(--mut);letter-spacing:.06em}
|
||||
.slider-row{display:flex;align-items:center;gap:14px}
|
||||
.val{min-width:78px;text-align:right;color:var(--ok);font-variant-numeric:tabular-nums}
|
||||
input[type=range]{-webkit-appearance:none;appearance:none;height:22px;background:transparent;outline:none;flex:1}
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
body{background:var(--bg);color:var(--fg);font-family:"JetBrains Mono",ui-monospace,Menlo,Consolas,monospace;
|
||||
font-size:13px;padding:18px 20px 26px}
|
||||
h1{font-size:16px;font-weight:600;letter-spacing:.5px}
|
||||
h1 small{color:var(--mut);font-size:11px;font-weight:400;margin-left:10px}
|
||||
.head{display:flex;align-items:baseline;justify-content:space-between;margin-bottom:14px;gap:12px}
|
||||
.status{color:var(--mut);font-size:11px}
|
||||
.card{background:var(--card);border:1px solid var(--bd);border-radius:12px;padding:14px 16px;margin-bottom:14px}
|
||||
.card h2{font-size:11px;font-weight:500;color:var(--mut);letter-spacing:1px;margin-bottom:12px;text-transform:uppercase}
|
||||
|
||||
/* ---- 电平表 ---- */
|
||||
.meters{display:grid;grid-template-columns:1fr 1fr;gap:10px 22px}
|
||||
.meter{display:flex;align-items:center;gap:9px}
|
||||
.meter .lbl{width:30px;color:var(--mut);font-size:11px;flex:none}
|
||||
.bar{position:relative;flex:1;height:12px;background:#111;border:1px solid var(--bd);border-radius:3px;overflow:hidden}
|
||||
.bar i{position:absolute;left:0;top:0;bottom:0;width:0%;
|
||||
background:linear-gradient(90deg,#166534,#4ade80 72%,#fbbf24 90%,#f87171);transition:width .08s linear}
|
||||
.meter .val{width:56px;text-align:right;font-size:11px;color:var(--fg);flex:none;font-variant-numeric:tabular-nums}
|
||||
.meter .pk{font-size:10px;color:var(--dim);width:52px;text-align:right;flex:none}
|
||||
|
||||
/* ---- 滑块 ---- */
|
||||
.row{display:flex;align-items:center;gap:14px;margin-bottom:12px}
|
||||
.row:last-child{margin-bottom:0}
|
||||
.row .name{width:96px;flex:none;color:var(--fg);font-size:12px}
|
||||
.row .num{width:84px;text-align:right;flex:none;color:var(--ok);font-variant-numeric:tabular-nums}
|
||||
input[type=range]{-webkit-appearance:none;appearance:none;flex:1;height:22px;background:transparent;cursor:ns-resize}
|
||||
input[type=range]::-webkit-slider-runnable-track{height:4px;background:var(--bd-hi);border-radius:2px}
|
||||
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:16px;height:16px;margin-top:-6px;
|
||||
border-radius:50%;background:var(--ok);border:0;cursor:grab;box-shadow:0 0 0 3px rgba(74,222,128,.15)}
|
||||
input[type=range]:active::-webkit-slider-thumb{cursor:grabbing}
|
||||
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:14px;height:14px;margin-top:-5px;
|
||||
border-radius:50%;background:var(--ok);border:none}
|
||||
input[type=range]:hover::-webkit-slider-thumb{background:#86efac}
|
||||
input[type=range]::-moz-range-track{height:4px;background:var(--bd-hi);border-radius:2px}
|
||||
input[type=range]::-moz-range-thumb{width:16px;height:16px;border:0;border-radius:50%;background:var(--ok)}
|
||||
.eqcard{background:var(--card);border:1px solid var(--bd);border-radius:var(--r);padding:16px 10px 10px}
|
||||
.eq{display:grid;grid-template-columns:repeat(32,1fr);gap:2px;height:230px}
|
||||
.band{display:flex;flex-direction:column;align-items:center;gap:4px}
|
||||
.band input[type=range]{writing-mode:vertical-lr;direction:rtl;width:22px;height:190px;flex:none}
|
||||
.band .db{font-size:9px;color:var(--dim);font-variant-numeric:tabular-nums;height:11px}
|
||||
.freqs{display:grid;grid-template-columns:repeat(32,1fr);gap:2px;margin-top:6px}
|
||||
.freqs span{font-size:9px;color:var(--dim);text-align:center;transform:rotate(-60deg);transform-origin:center;
|
||||
white-space:nowrap;height:26px}
|
||||
.actions{display:flex;gap:10px;margin-top:14px;align-items:center}
|
||||
button{background:var(--card-hi);color:var(--fg);border:1px solid var(--bd-hi);border-radius:8px;
|
||||
padding:7px 14px;font:inherit;font-size:12px;cursor:pointer}
|
||||
input[type=range]::-moz-range-thumb{width:14px;height:14px;border:none;border-radius:50%;background:var(--ok)}
|
||||
.hint{color:var(--dim);font-size:10px;margin-top:8px}
|
||||
|
||||
/* ---- EQ ---- */
|
||||
.eq{display:flex;align-items:flex-end;gap:3px;height:190px;padding-top:6px}
|
||||
.band{flex:1;display:flex;flex-direction:column;align-items:center;gap:4px;height:100%}
|
||||
.band .v{font-size:9px;color:var(--dim);height:11px;font-variant-numeric:tabular-nums}
|
||||
.band input{writing-mode:vertical-lr;direction:rtl;width:100%;height:100%;
|
||||
-webkit-appearance:none;appearance:none;background:transparent;cursor:ns-resize}
|
||||
.band input::-webkit-slider-runnable-track{width:4px;height:100%;background:var(--bd-hi);border-radius:2px}
|
||||
.band input::-webkit-slider-thumb{-webkit-appearance:none;width:14px;height:9px;margin-left:-5px;
|
||||
border-radius:2px;background:var(--ok)}
|
||||
.band:hover input::-webkit-slider-thumb{background:#86efac}
|
||||
.band input::-moz-range-track{width:4px;background:var(--bd-hi);border-radius:2px}
|
||||
.band input::-moz-range-thumb{width:14px;height:9px;border:none;border-radius:2px;background:var(--ok)}
|
||||
.band .f{font-size:9px;color:var(--mut);transform:rotate(-60deg);transform-origin:center;height:20px;
|
||||
white-space:nowrap;margin-top:2px}
|
||||
.btns{display:flex;gap:8px;margin-top:12px}
|
||||
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)}
|
||||
button.lit{background:var(--ok-bg);border-color:var(--ok-bd);color:#bbf7d0}
|
||||
.toggle{display:flex;align-items:center;gap:7px;color:var(--mut);font-size:12px;cursor:pointer}
|
||||
.state{margin-left:auto;color:var(--dim);font-size:11px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header>
|
||||
<h1>Collaplex 音效</h1>
|
||||
<span class="hint">滚轮调滑块 · Shift+滚轮微调 · 双击归零</span>
|
||||
<div class="meter">
|
||||
<span>入 <b id="lvl-in">-∞</b> dBFS</span>
|
||||
<span>出 <b id="lvl-out">-∞</b> dBFS</span>
|
||||
</div>
|
||||
</header>
|
||||
<div class="head">
|
||||
<h1>Collaplex 音效<small>滚轮调节 · Shift+滚轮微调 · 双击归零</small></h1>
|
||||
<div class="status" id="status">就绪</div>
|
||||
</div>
|
||||
|
||||
<div class="rows">
|
||||
<div class="card">
|
||||
<h2>电平归一化(响度目标)</h2>
|
||||
<div class="slider-row">
|
||||
<input type="range" id="loud" min="-40" max="-5" step="0.5" value="-16">
|
||||
<span class="val" id="loud-val">-16.0 dB</span>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<label class="toggle"><input type="checkbox" id="loud-on" checked> 启用</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>总音量放大</h2>
|
||||
<div class="slider-row">
|
||||
<input type="range" id="vol" min="-12" max="12" step="0.5" value="6">
|
||||
<span class="val" id="vol-val">+6.0 dB</span>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button id="reset-all">全部归零</button>
|
||||
<button id="reset-eq">EQ 归零</button>
|
||||
<span class="state" id="state">—</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>电平</h2>
|
||||
<div class="meters">
|
||||
<div class="meter"><span class="lbl">入 L</span><div class="bar"><i id="bar-inL"></i></div><span class="val" id="val-inL">-∞</span><span class="pk" id="pk-inL"></span></div>
|
||||
<div class="meter"><span class="lbl">出 L</span><div class="bar"><i id="bar-outL"></i></div><span class="val" id="val-outL">-∞</span><span class="pk" id="pk-outL"></span></div>
|
||||
<div class="meter"><span class="lbl">入 R</span><div class="bar"><i id="bar-inR"></i></div><span class="val" id="val-inR">-∞</span><span class="pk" id="pk-inR"></span></div>
|
||||
<div class="meter"><span class="lbl">出 R</span><div class="bar"><i id="bar-outR"></i></div><span class="val" id="val-outR">-∞</span><span class="pk" id="pk-outR"></span></div>
|
||||
</div>
|
||||
<div class="hint">条长 = 实时 RMS(-60 ~ 0 dBFS),右侧小字 = 本块峰值</div>
|
||||
</div>
|
||||
|
||||
<div class="eqcard">
|
||||
<div class="eq" id="eq"></div>
|
||||
<div class="freqs" id="freqs"></div>
|
||||
<div class="card">
|
||||
<h2>控制</h2>
|
||||
<div class="row">
|
||||
<span class="name">电平归一化</span>
|
||||
<input type="range" id="loud" min="-30" max="-6" step="0.5">
|
||||
<span class="num" id="loud-num">—</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="name">混响湿度</span>
|
||||
<input type="range" id="wet" min="0" max="1" step="0.01">
|
||||
<span class="num" id="wet-num">—</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="name">总音量</span>
|
||||
<input type="range" id="vol" min="-12" max="12" step="0.5">
|
||||
<span class="num" id="vol-num">—</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>32 段 EQ(1/3 倍频程 · ±15 dB)</h2>
|
||||
<div class="eq" id="eq"></div>
|
||||
<div class="btns">
|
||||
<button id="flat">全部归零</button>
|
||||
<button id="reset">重新读取</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const el = (id) => document.getElementById(id);
|
||||
let gains = new Array(32).fill(0);
|
||||
let freqs = [];
|
||||
let pending = null;
|
||||
let state = null;
|
||||
const post = async (path, body) => {
|
||||
const r = await fetch(path, {method:"POST", headers:{"Content-Type":"application/json"}, body:JSON.stringify(body)});
|
||||
return r.ok;
|
||||
};
|
||||
const say = (t) => { el("status").textContent = t; };
|
||||
|
||||
function fmt(v, plus) {
|
||||
const s = (v > 0 && plus ? "+" : "") + v.toFixed(1);
|
||||
return s + " dB";
|
||||
}
|
||||
|
||||
// 滑块 + 滚轮捕捉
|
||||
function bindWheel(el, onChange) {
|
||||
el.addEventListener("wheel", (e) => {
|
||||
/* ---------- 滚轮捕获 ---------- */
|
||||
function bindWheel(input, onDelta, onDone) {
|
||||
input.addEventListener("wheel", (e) => {
|
||||
e.preventDefault();
|
||||
const step = e.shiftKey ? 0.1 : 0.5;
|
||||
const dir = e.deltaY < 0 ? 1 : -1;
|
||||
const next = parseFloat(el.value) + dir * step;
|
||||
el.value = Math.min(parseFloat(el.max), Math.max(parseFloat(el.min), next));
|
||||
onChange(parseFloat(el.value));
|
||||
}, { passive: false });
|
||||
const step = parseFloat(input.step) || 1;
|
||||
const fine = e.shiftKey ? step / 5 : step;
|
||||
const next = Math.min(parseFloat(input.max), Math.max(parseFloat(input.min),
|
||||
parseFloat(input.value) + (e.deltaY < 0 ? fine : -fine)));
|
||||
input.value = next;
|
||||
onDelta(next);
|
||||
if (onDone) clearTimeout(input._t), input._t = setTimeout(onDone, 90);
|
||||
}, {passive:false});
|
||||
}
|
||||
|
||||
function throttled(fn, ms) {
|
||||
let last = 0, timer = null, lastArgs = null;
|
||||
return function (...args) {
|
||||
lastArgs = args;
|
||||
const now = Date.now();
|
||||
const run = () => { last = Date.now(); timer = null; fn(...lastArgs); };
|
||||
if (now - last >= ms) run();
|
||||
else if (!timer) timer = setTimeout(run, ms - (now - last));
|
||||
};
|
||||
/* ---------- 三个控制滑块 ---------- */
|
||||
const loud = el("loud"), wet = el("wet"), vol = el("vol");
|
||||
loud.addEventListener("input", () => el("loud-num").textContent = (+loud.value).toFixed(1) + " dB");
|
||||
wet.addEventListener("input", () => el("wet-num").textContent = (+wet.value).toFixed(2));
|
||||
vol.addEventListener("input", () => el("vol-num").textContent = (+vol.value >= 0 ? "+" : "") + (+vol.value).toFixed(1) + " dB");
|
||||
loud.addEventListener("change", async () => { await post("/api/loudness", {target_db:+loud.value}); say("归一化已生效"); });
|
||||
wet.addEventListener("change", async () => { await post("/api/wet", {wet:+wet.value}); say("湿度已生效"); });
|
||||
vol.addEventListener("change", async () => { await post("/api/volume", {db:+vol.value}); say("总音量已生效"); });
|
||||
bindWheel(loud, v => el("loud-num").textContent = v.toFixed(1) + " dB", () => loud.dispatchEvent(new Event("change")));
|
||||
bindWheel(wet, v => el("wet-num").textContent = v.toFixed(2), () => wet.dispatchEvent(new Event("change")));
|
||||
bindWheel(vol, v => el("vol-num").textContent = (v >= 0 ? "+" : "") + v.toFixed(1) + " dB", () => vol.dispatchEvent(new Event("change")));
|
||||
[loud, wet, vol].forEach(s => s.addEventListener("dblclick", () => {
|
||||
s.value = s.dataset.def; s.dispatchEvent(new Event("input")); s.dispatchEvent(new Event("change"));
|
||||
}));
|
||||
loud.dataset.def = -16; wet.dataset.def = 0.3; vol.dataset.def = 0;
|
||||
|
||||
/* ---------- 32 段 EQ ---------- */
|
||||
const eqBox = el("eq");
|
||||
let commitTimer = null;
|
||||
function commitEq() {
|
||||
clearTimeout(commitTimer);
|
||||
commitTimer = setTimeout(async () => {
|
||||
const ok = await post("/api/eq", {gains});
|
||||
say(ok ? "EQ 已生效" : "EQ 写入失败");
|
||||
}, 90);
|
||||
}
|
||||
|
||||
const pushEq = throttled(async () => {
|
||||
setState("写入 EQ…");
|
||||
try {
|
||||
const r = await fetch("/api/eq", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ gains })
|
||||
});
|
||||
setState(r.ok ? "EQ 已生效" : "EQ 写入失败");
|
||||
} catch (err) { setState("EQ 写入失败"); }
|
||||
}, 90);
|
||||
|
||||
const pushVol = throttled(async (db) => {
|
||||
setState("写入总音量…");
|
||||
try {
|
||||
const r = await fetch("/api/volume", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ db })
|
||||
});
|
||||
setState(r.ok ? "总音量已生效" : "写入失败");
|
||||
} catch (err) { setState("写入失败"); }
|
||||
}, 90);
|
||||
|
||||
const pushLoud = throttled(async (db, on) => {
|
||||
setState("写入归一化…");
|
||||
try {
|
||||
const r = await fetch("/api/loudness", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ target_db: db, on })
|
||||
});
|
||||
setState(r.ok ? "归一化已生效" : "写入失败");
|
||||
} catch (err) { setState("写入失败"); }
|
||||
}, 90);
|
||||
|
||||
function setState(text) { $("state").textContent = text; }
|
||||
|
||||
// 32 段 EQ 滑块
|
||||
function buildEq() {
|
||||
const box = $("eq"), fbox = $("freqs");
|
||||
box.innerHTML = ""; fbox.innerHTML = "";
|
||||
for (let i = 0; i < 32; i++) {
|
||||
state = null;
|
||||
function buildEq(freqs) {
|
||||
eqBox.innerHTML = "";
|
||||
freqs.forEach((f, i) => {
|
||||
const band = document.createElement("div");
|
||||
band.className = "band";
|
||||
const db = document.createElement("div");
|
||||
db.className = "db"; db.id = "db" + i; db.textContent = "0.0";
|
||||
const sl = document.createElement("input");
|
||||
sl.type = "range"; sl.min = "-15"; sl.max = "15"; sl.step = "0.5"; sl.value = "0";
|
||||
sl.dataset.index = i;
|
||||
sl.addEventListener("input", () => {
|
||||
gains[i] = parseFloat(sl.value);
|
||||
$("db" + i).textContent = gains[i].toFixed(1);
|
||||
pushEq();
|
||||
const v = document.createElement("div"); v.className = "v"; v.textContent = "0";
|
||||
const input = document.createElement("input");
|
||||
input.type = "range"; input.min = -15; input.max = 15; input.step = 0.5; input.value = 0;
|
||||
input.dataset.index = i;
|
||||
const label = document.createElement("div"); label.className = "f";
|
||||
label.textContent = f >= 1000 ? (f / 1000) + "k" : f;
|
||||
input.addEventListener("input", () => {
|
||||
const val = parseFloat(input.value);
|
||||
v.textContent = val === 0 ? "" : (val > 0 ? "+" : "") + val.toFixed(1);
|
||||
gains[i] = val;
|
||||
commitEq();
|
||||
});
|
||||
bindWheel(sl, (v) => {
|
||||
gains[i] = v; $("db" + i).textContent = v.toFixed(1); pushEq();
|
||||
});
|
||||
sl.addEventListener("dblclick", () => {
|
||||
sl.value = "0"; gains[i] = 0; $("db" + i).textContent = "0.0"; pushEq();
|
||||
});
|
||||
band.appendChild(db); band.appendChild(sl);
|
||||
box.appendChild(band);
|
||||
const f = document.createElement("span");
|
||||
f.textContent = freqs[i] ? (freqs[i] >= 1000 ? (freqs[i] / 1000) + "k" : String(freqs[i])) : "";
|
||||
fbox.appendChild(f);
|
||||
bindWheel(input, (val) => { input.value = val; input.dispatchEvent(new Event("input")); });
|
||||
input.addEventListener("dblclick", () => { input.value = 0; input.dispatchEvent(new Event("input")); });
|
||||
band.append(v, input, label);
|
||||
eqBox.appendChild(band);
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- 电平表 ---------- */
|
||||
const METERS = [
|
||||
["inL", "in_rms", 0], ["outL", "out_rms", 0],
|
||||
["inR", "in_rms", 1], ["outR", "out_rms", 1],
|
||||
];
|
||||
const PK = [["pk-inL","in_peak",0],["pk-outL","out_peak",0],["pk-inR","in_peak",1],["pk-outR","out_peak",1]];
|
||||
function fmt(db) { return db <= -119 ? "-∞" : db.toFixed(1); }
|
||||
function updateMeters(levels) {
|
||||
if (!levels) return;
|
||||
for (const [id, key, ch] of METERS) {
|
||||
const db = (levels[key] || [-120, -120])[ch];
|
||||
const pct = Math.max(0, Math.min(100, (db + 60) / 60 * 100));
|
||||
el("bar-" + id).style.width = pct + "%";
|
||||
el("val-" + id).textContent = fmt(db);
|
||||
}
|
||||
for (const [id, key, ch] of PK) {
|
||||
const db = (levels[key] || [-120, -120])[ch];
|
||||
el(id).textContent = db <= -119 ? "" : fmt(db);
|
||||
}
|
||||
}
|
||||
|
||||
// 同步 DSP 实时电平
|
||||
async function poll() {
|
||||
/* ---------- 轮询 ---------- */
|
||||
async function tick() {
|
||||
try {
|
||||
const s = await (await fetch("/api/state")).json();
|
||||
$("lvl-in").textContent = (s.levels.in_rms <= -120 ? "-∞" : s.levels.in_rms.toFixed(1));
|
||||
$("lvl-out").textContent = (s.levels.out_rms <= -120 ? "-∞" : s.levels.out_rms.toFixed(1));
|
||||
} catch (err) { /* 忽略 */ }
|
||||
const r = await fetch("/api/state");
|
||||
const d = await r.json();
|
||||
if (state === null) {
|
||||
buildEq(d.freqs);
|
||||
gains = d.gains.slice();
|
||||
eqBox.querySelectorAll("input").forEach((inp) => {
|
||||
inp.value = gains[+inp.dataset.index];
|
||||
const v = inp.previousElementSibling;
|
||||
const val = parseFloat(inp.value);
|
||||
v.textContent = val === 0 ? "" : (val > 0 ? "+" : "") + val.toFixed(1);
|
||||
});
|
||||
}
|
||||
if (document.activeElement !== vol) { vol.value = d.volume_db; el("vol-num").textContent = (d.volume_db >= 0 ? "+" : "") + d.volume_db.toFixed(1) + " dB"; }
|
||||
if (document.activeElement !== wet) { wet.value = d.wet; el("wet-num").textContent = (+d.wet).toFixed(2); }
|
||||
if (document.activeElement !== loud && d.loudness) { loud.value = d.loudness.target_db; el("loud-num").textContent = (+d.loudness.target_db).toFixed(1) + " dB"; }
|
||||
updateMeters(d.levels);
|
||||
state = d;
|
||||
} catch (e) { say("面板与 DSP 失联"); }
|
||||
}
|
||||
|
||||
async function init() {
|
||||
const s = await (await fetch("/api/state")).json();
|
||||
freqs = s.freqs;
|
||||
gains = s.gains.slice();
|
||||
buildEq();
|
||||
for (let i = 0; i < 32; i++) {
|
||||
const sl = document.querySelector('.band input[data-index="' + i + '"]');
|
||||
sl.value = String(gains[i]);
|
||||
$("db" + i).textContent = gains[i].toFixed(1);
|
||||
}
|
||||
const loud = s.loudness || { target_db: -16, on: true };
|
||||
$("loud").value = loud.target_db; $("loud-val").textContent = fmt(loud.target_db, false);
|
||||
$("loud-on").checked = loud.on !== false;
|
||||
$("vol").value = s.volume_db; $("vol-val").textContent = fmt(s.volume_db, true);
|
||||
|
||||
$("loud").addEventListener("input", (e) => {
|
||||
const v = parseFloat(e.target.value);
|
||||
$("loud-val").textContent = fmt(v, false);
|
||||
pushLoud(v, $("loud-on").checked);
|
||||
});
|
||||
bindWheel($("loud"), (v) => { $("loud-val").textContent = fmt(v, false); pushLoud(v, $("loud-on").checked); });
|
||||
$("loud-on").addEventListener("change", (e) => pushLoud(parseFloat($("loud").value), e.target.checked));
|
||||
|
||||
$("vol").addEventListener("input", (e) => {
|
||||
const v = parseFloat(e.target.value);
|
||||
$("vol-val").textContent = fmt(v, true);
|
||||
pushVol(v);
|
||||
});
|
||||
bindWheel($("vol"), (v) => { $("vol-val").textContent = fmt(v, true); pushVol(v); });
|
||||
|
||||
$("reset-eq").addEventListener("click", () => {
|
||||
gains = new Array(32).fill(0);
|
||||
for (let i = 0; i < 32; i++) { document.querySelector('.band input[data-index="' + i + '"]').value = "0"; $("db" + i).textContent = "0.0"; }
|
||||
pushEq();
|
||||
});
|
||||
$("reset-all").addEventListener("click", () => {
|
||||
$("reset-eq").click();
|
||||
$("vol").value = "0"; $("vol-val").textContent = fmt(0, true); pushVol(0);
|
||||
});
|
||||
|
||||
setInterval(poll, 700);
|
||||
poll();
|
||||
}
|
||||
|
||||
init();
|
||||
el("flat").addEventListener("click", () => { gains = new Array(32).fill(0); commitEq(); buildEq(state ? state.freqs : []); });
|
||||
el("reset").addEventListener("click", () => { state = null; tick(); });
|
||||
tick();
|
||||
setInterval(tick, 250);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user