Files
archived-vdo.ninja/examples/noisegate.html
2025-10-21 20:52:45 -04:00

358 lines
16 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Noise Gate Converter — Classic → "My Gate"</title>
<style>
:root {
--bg: #0e1116;
--panel: #171b23;
--muted: #9aa4b2;
--text: #e7edf3;
--accent: #6ee7b7;
--accent-2: #60a5fa;
--danger: #f87171;
}
* { box-sizing: border-box; }
body {
margin: 0; background: radial-gradient(1200px 600px at 20% -10%, #131826 0%, #0f1320 45%, var(--bg) 100%);
color: var(--text); font: 14px/1.35 ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell;
}
.wrap { max-width: 1100px; margin: 32px auto; padding: 0 16px; }
header { display: flex; gap: 16px; align-items: center; justify-content: space-between; }
h1 { font-size: 22px; margin: 0; letter-spacing: 0.2px; }
.card { background: linear-gradient(180deg, rgba(255,255,255,.02), rgba(255,255,255,.0) 30%), var(--panel);
border: 1px solid rgba(255,255,255,.06); border-radius: 16px; padding: 16px; box-shadow: 0 10px 30px rgba(0,0,0,.25); }
.grid { display: grid; grid-template-columns: 1.1fr 1fr; gap: 16px; }
.inputs { display: grid; grid-template-columns: repeat(2, minmax(180px, 1fr)); gap: 12px; }
label { display: block; font-weight: 600; color: #cdd6e1; margin-bottom: 6px; }
input[type="number"] { width: 100%; padding: 10px 12px; border-radius: 10px; border: 1px solid rgba(255,255,255,.08);
background: #0e1320; color: var(--text); outline: none; }
input[type="number"]:focus { border-color: var(--accent-2); box-shadow: 0 0 0 3px rgba(96,165,250,.25); }
.row { display: flex; gap: 10px; align-items: center; }
.muted { color: var(--muted); }
.pill { padding: 4px 10px; border-radius: 999px; border: 1px solid rgba(255,255,255,.08); background: rgba(255,255,255,.04); }
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; font-size: 12.5px; }
pre { margin: 0; white-space: pre-wrap; word-break: break-word; }
button { cursor: pointer; border: 0; padding: 10px 14px; border-radius: 12px; color: #0a0d12; background: var(--accent);
font-weight: 700; letter-spacing: .2px; }
button.secondary { background: #222836; color: #e8eef6; border: 1px solid rgba(255,255,255,.08); }
button.danger { background: var(--danger); color: #0a0d12; }
button:disabled { opacity: .6; cursor: not-allowed; }
.section-title { font-size: 13px; color: #a3b0c2; text-transform: uppercase; letter-spacing: .12em; margin: 16px 0 8px; }
/* meters */
.meters { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 12px; }
.meter { position: relative; height: 16px; border-radius: 999px; background: #121826;
border: 1px solid rgba(255,255,255,.08); overflow: hidden; }
.meter .fill { position: absolute; left: 0; top: 0; bottom: 0; width: 0%; background: linear-gradient(90deg, var(--accent-2), var(--accent));
transition: width .08s linear; }
.foot { margin-top: 14px; color: #90a1b6; font-size: 12.5px; }
.cols { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.code-block { background: #0b0f19; border: 1px solid rgba(255,255,255,.08); padding: 12px; border-radius: 12px; }
.caption { color: #9eb0c6; font-size: 12.5px; margin: 8px 0 2px; }
.kbd { font-family: ui-monospace, monospace; background: #0e1422; border: 1px solid rgba(255,255,255,.08); border-bottom-width: 2px; padding: 2px 6px; border-radius: 6px; }
</style>
</head>
<body>
<div class="wrap">
<header>
<h1>Classic → <span class="pill mono">My Gate</span> converter</h1>
<div class="row">
<button id="startBtn" class="secondary">Start live test</button>
<button id="resetBtn" class="secondary" title="Reset to sensible defaults">Reset</button>
</div>
</header>
<div class="grid" style="margin-top: 16px;">
<section class="card">
<div class="section-title">Inputs — classic noise gate</div>
<div class="inputs">
<div>
<label for="thr">Threshold (dBFS)</label>
<input id="thr" type="number" step="1" min="-120" max="0" value="-50" />
<div class="muted" style="margin-top:6px">Linear amplitude: <span id="linThresh" class="mono">0.003162</span></div>
</div>
<div>
<label for="att">Attack (ms)</label>
<input id="att" type="number" step="1" min="0" max="2000" value="1" />
</div>
<div>
<label for="rel">Release (ms)</label>
<input id="rel" type="number" step="1" min="0" max="4000" value="100" />
</div>
<div>
<label for="hold">Hold (ms)</label>
<input id="hold" type="number" step="1" min="0" max="4000" value="10" />
</div>
<div>
<label for="range">Range / Depth (% reduction when closed)</label>
<input id="range" type="number" step="1" min="0" max="100" value="80" />
<div class="muted" style="margin-top:6px">Closed gain: <span id="downGain" class="mono">20%</span></div>
</div>
</div>
<div class="section-title">Live meter (optional)</div>
<div class="meters">
<div>
<div class="muted">Input level (approx dBFS)</div>
<div class="meter"><div id="inFill" class="fill" style="--v:0"></div></div>
</div>
<div>
<div class="muted">Gate gain (%)</div>
<div class="meter"><div id="gainFill" class="fill" style="--v:100"></div></div>
</div>
</div>
<div class="foot">Tip: set <span class="kbd">Range</span> for how deep your gate closes. Classic gates often call this <em>Range</em> or <em>Depth</em>. The rest maps 1:1.</div>
</section>
<section class="card">
<div class="section-title">Outputs — what your code expects</div>
<div class="cols">
<div>
<div class="caption">Calls you make when the detector toggles</div>
<div class="code-block mono" id="callsBlock">
<pre>// gate CLOSE (after Hold):
<span id="gateDownCall">changeGatingGain(20, 100)</span>
// gate OPEN (on speech):
<span id="gateUpCall">changeGatingGain(100, 1)</span></pre>
</div>
</div>
<div>
<div class="caption">URL params / compact settings string</div>
<div class="code-block mono">
<pre>?noisegate=1&amp;noisegatesettings=<span id="settingsStr">-50,1,100,10,20</span></pre>
</div>
<div class="caption">Individual values</div>
<div class="code-block mono" id="kv">
<pre>{
thresholdDb: <span id="kv_thr">-50</span>,
attackMs: <span id="kv_att">1</span>,
releaseMs: <span id="kv_rel">100</span>,
holdMs: <span id="kv_hold">10</span>,
closedGainPercent: <span id="kv_closed">20</span>
}</pre>
</div>
<div class="row" style="margin-top:8px; gap:8px;">
<button class="secondary" id="copyUrl">Copy URL</button>
<button class="secondary" id="copyCalls">Copy calls</button>
</div>
</div>
</div>
<div class="section-title">Dropin helper (optional)</div>
<div class="code-block mono" style="max-height: 320px; overflow: auto;">
<pre id="helperCode">// Convert classic gate knobs to your changeGatingGain() usage.
function classicalToMyGate({ thresholdDb, attackMs, releaseMs, holdMs, rangePct = 80 }) {
const clamp = (x, a, b) => Math.min(b, Math.max(a, x));
const downGainPercent = 100 - clamp(rangePct, 0, 100); // % of full when closed
const linearThreshold = Math.pow(10, thresholdDb / 20);
return {
thresholdDb,
linearThreshold, // for detector if you need it
attackMs: Math.max(0, attackMs|0),
releaseMs: Math.max(0, releaseMs|0),
holdMs: Math.max(0, holdMs|0),
downGainPercent, // e.g., 20 means -80% depth
gateDownCall: `changeGatingGain(${downGainPercent}, ${releaseMs|0})`,
gateUpCall: `changeGatingGain(100, ${attackMs|0})`,
// compact string your app can parse
noisegatesettings: [thresholdDb, attackMs|0, releaseMs|0, holdMs|0, downGainPercent|0].join(',')
};
}
// Simple detector that drives your changeGatingGain() based on an AnalyserNode.
// Uses threshold + hold; release/attack are handled by changeGatingGain ramps.
function wireClassicGateDetector({ analyser, audioCtx, thresholdDb, holdMs, attackMs, releaseMs, downGainPercent }) {
const buf = new Float32Array(analyser.fftSize);
let state = 'open';
let holdUntil = 0;
function setGainPct(percent, ms) {
const t = audioCtx.currentTime;
const v = percent / 100;
try {
// mirrors your changeGatingGain() behavior
analyser.disconnect; // no-op keeps linter happy
window.changeGatingGain ? window.changeGatingGain(percent, ms) : null;
} catch (e) {}
}
function tick() {
analyser.getFloatTimeDomainData(buf);
let s = 0; for (let i = 0; i < buf.length; i++) s += buf[i] * buf[i];
const rms = Math.sqrt(s / buf.length) + 1e-12;
const db = 20 * Math.log10(rms);
const now = performance.now();
if (db > thresholdDb) {
holdUntil = now + holdMs;
if (state !== 'open') { setGainPct(100, attackMs); state = 'open'; }
} else if (now > holdUntil) {
if (state !== 'closed') { setGainPct(100 - downGainPercent, releaseMs); state = 'closed'; }
}
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
}
</pre>
</div>
</section>
</div>
</div>
<script>
// ==== Utility ==== //
const $ = (id) => document.getElementById(id);
const clamp = (x, a, b) => Math.min(b, Math.max(a, x));
const dbToLin = (db) => Math.pow(10, db / 20);
function compute() {
const thr = parseFloat($("thr").value || -50);
const att = parseInt($("att").value || 0, 10);
const rel = parseInt($("rel").value || 0, 10);
const hold = parseInt($("hold").value || 0, 10);
const range = clamp(parseInt($("range").value || 80, 10), 0, 100);
const downGainPercent = 100 - range; // % of full volume when closed
const cfg = {
thresholdDb: thr,
linearThreshold: dbToLin(thr),
attackMs: att, releaseMs: rel, holdMs: hold,
downGainPercent
};
$("linThresh").textContent = cfg.linearThreshold.toFixed(6);
$("downGain").textContent = (cfg.downGainPercent).toFixed(0) + '%';
$("gateDownCall").textContent = `changeGatingGain(${cfg.downGainPercent}, ${cfg.releaseMs})`;
$("gateUpCall").textContent = `changeGatingGain(100, ${cfg.attackMs})`;
$("settingsStr").textContent = [cfg.thresholdDb, cfg.attackMs, cfg.releaseMs, cfg.holdMs, cfg.downGainPercent].join(',');
$("kv_thr").textContent = cfg.thresholdDb;
$("kv_att").textContent = cfg.attackMs;
$("kv_rel").textContent = cfg.releaseMs;
$("kv_hold").textContent = cfg.holdMs;
$("kv_closed").textContent = cfg.downGainPercent;
return cfg;
}
// bind inputs
Array.from(document.querySelectorAll('input')).forEach(i => i.addEventListener('input', compute));
compute();
// Copy helpers
$("copyUrl").addEventListener('click', () => {
const s = `?noisegate=1&noisegatesettings=${$("settingsStr").textContent}`;
navigator.clipboard.writeText(s);
notify('Copied URL params');
});
$("copyCalls").addEventListener('click', () => {
const s = `${$("gateDownCall").textContent}\n${$("gateUpCall").textContent}\n`;
navigator.clipboard.writeText(s);
notify('Copied function calls');
});
function notify(msg) {
const el = document.createElement('div');
el.textContent = msg; el.style.position = 'fixed'; el.style.right = '16px'; el.style.bottom = '16px';
el.style.background = 'rgba(20,26,37,.95)'; el.style.border = '1px solid rgba(255,255,255,.08)'; el.style.padding = '10px 12px'; el.style.borderRadius = '10px';
document.body.appendChild(el); setTimeout(() => el.remove(), 1200);
}
$("resetBtn").addEventListener('click', () => {
$("thr").value = -50; $("att").value = 1; $("rel").value = 100; $("hold").value = 10; $("range").value = 80; compute();
});
// ==== Live test (optional) ==== //
let ctx, src, gateNode, analyser, raf, buf;
let state = 'open', holdUntil = 0;
const inFill = $("inFill"), gainFill = $("gainFill");
async function start() {
if (ctx) return stop();
try {
ctx = new (window.AudioContext || window.webkitAudioContext)();
const stream = await navigator.mediaDevices.getUserMedia({audio: { echoCancellation:false, noiseSuppression:false, autoGainControl:false }});
src = ctx.createMediaStreamSource(stream);
gateNode = ctx.createGain(); gateNode.gain.value = 1.0;
src.connect(gateNode);
analyser = ctx.createAnalyser(); analyser.fftSize = 2048; buf = new Float32Array(analyser.fftSize);
gateNode.connect(analyser); gateNode.connect(ctx.destination);
loop();
$("startBtn").textContent = 'Stop live test';
} catch (e) {
notify('Mic permission denied or unavailable');
}
}
function stop() {
cancelAnimationFrame(raf); raf = null;
if (ctx) { try { ctx.close(); } catch (e) {} }
ctx = src = gateNode = analyser = null; state = 'open';
inFill.style.width = '0%'; gainFill.style.width = '100%';
$("startBtn").textContent = 'Start live test';
}
function setGainPct(pct, ms) {
if (!gateNode) return;
const t = ctx.currentTime; const v = clamp(pct,0,100)/100;
try {
gateNode.gain.cancelScheduledValues(t);
gateNode.gain.setValueAtTime(gateNode.gain.value, t);
gateNode.gain.linearRampToValueAtTime(v, t + Math.max(0, ms)/1000);
} catch (e) {
gateNode.gain.value = v;
}
}
function loop() {
const cfg = compute();
analyser.getFloatTimeDomainData(buf);
let s = 0; for (let i = 0; i < buf.length; i++) s += buf[i]*buf[i];
const rms = Math.sqrt(s/buf.length) + 1e-12;
const db = 20*Math.log10(rms);
// simple display: map -100..0 dB to 0..100
const inPct = clamp(100 + db, 0, 100);
inFill.style.width = inPct + '%';
const now = performance.now();
if (db > cfg.thresholdDb) {
holdUntil = now + cfg.holdMs;
if (state !== 'open') { setGainPct(100, cfg.attackMs); state = 'open'; }
} else if (now > holdUntil) {
if (state !== 'closed') { setGainPct(cfg.downGainPercent, cfg.releaseMs); state = 'closed'; }
}
const gv = gateNode.gain.value * 100; gainFill.style.width = clamp(gv, 0, 100) + '%';
raf = requestAnimationFrame(loop);
}
$("startBtn").addEventListener('click', () => ctx ? stop() : start());
// Expose the core converter globally in case you want to copy it out via DevTools
window.classicalToMyGate = function(args){
const range = clamp(args.rangePct ?? 80, 0, 100);
const downGainPercent = 100 - range;
return {
thresholdDb: args.thresholdDb,
linearThreshold: dbToLin(args.thresholdDb),
attackMs: args.attackMs|0,
releaseMs: args.releaseMs|0,
holdMs: args.holdMs|0,
downGainPercent,
gateDownCall: `changeGatingGain(${downGainPercent}, ${args.releaseMs|0})`,
gateUpCall: `changeGatingGain(100, ${args.attackMs|0})`,
noisegatesettings: [args.thresholdDb, args.attackMs|0, args.releaseMs|0, args.holdMs|0, downGainPercent|0].join(',')
};
};
</script>
</body>
</html>