diff --git a/web/src/debug.ts b/web/src/debug.ts index 7d975c4..6af32d6 100644 --- a/web/src/debug.ts +++ b/web/src/debug.ts @@ -5,20 +5,32 @@ import type { Features } from "./features"; const KEYS: (keyof Features)[] = ["sub", "low", "mid", "high", "kick", "snare", "wobble", "drop"]; +/// Onsets counted since the overlay opened; also on `window.audioDebug` for scripts. +const counts = { kicks: 0, snares: 0, frames: 0 }; + export function audioDebug(): (f: Features) => void { + (window as unknown as { audioDebug: typeof counts }).audioDebug = counts; + let lastAge = 99, lastSnare = 0; const canvas = document.createElement("canvas"); canvas.className = "audio-debug"; - const w = (canvas.width = 240), h = (canvas.height = 110); + const w = (canvas.width = 240), h = (canvas.height = 124); document.body.append(canvas); const ctx = canvas.getContext("2d")!; ctx.font = "10px system-ui, sans-serif"; const bar = w / KEYS.length; return (f) => { + counts.frames++; + if (f.kickAge < lastAge) counts.kicks++; + if (f.snare > lastSnare + 0.5) counts.snares++; + lastAge = f.kickAge; + lastSnare = f.snare; ctx.clearRect(0, 0, w, h); + ctx.fillStyle = "#fff9"; + ctx.fillText(`kicks ${counts.kicks} · snares ${counts.snares}`, 4, 11); KEYS.forEach((k, i) => { const v = Math.min(1, Math.max(0, f[k])); ctx.fillStyle = k === "kick" || k === "snare" ? "#ff4d6d" : k === "drop" ? "#ffb02e" : "#7fd4ff"; - ctx.fillRect(i * bar + 3, h - 14 - v * (h - 20), bar - 6, v * (h - 20)); + ctx.fillRect(i * bar + 3, h - 14 - v * (h - 30), bar - 6, v * (h - 30)); ctx.fillStyle = "#fff9"; ctx.fillText(k, i * bar + 3, h - 3); }); diff --git a/web/src/features.ts b/web/src/features.ts index 04678a3..3df2ec9 100644 --- a/web/src/features.ts +++ b/web/src/features.ts @@ -43,6 +43,7 @@ const SNARE_DECAY = 0.12; // s const WOBBLE_SMOOTH = 0.12; // s const DROP_SHORT = 1; // s of low-end loudness … const DROP_LONG = 12; // … against this much +const DROP_HEARD_DB = -70; // the low end counts as sounding above this const DROP_RISE_DB = 2.5; // dB above the long average where a drop starts const DROP_FULL_DB = 5; // dB above the start for a full drop const DROP_ATTACK = 0.25; // s @@ -103,6 +104,9 @@ export class FeatureTracker { private wobblePeak = 1; private short = SILENCE_DB; private long = SILENCE_DB; + /// Seconds of sound heard so far: the long average covers no more than that, so + /// music that starts in the middle of a drop is the baseline, not a drop. + private heard = 0; /// One frame from the analyser. fromAnalyser(an: AnalyserNode, now: number, dt: number): Features { @@ -172,10 +176,13 @@ export class FeatureTracker { this.wobblePeak = Math.max(1, speed, this.wobblePeak * Math.exp(-dt / 4)); f.wobble += (clamp01(speed / this.wobblePeak) - f.wobble) * ease(dt, WOBBLE_SMOOTH); - // Start from the first sound heard, or every track would open with a drop. - if (this.long <= SILENCE_DB) this.short = this.long = lowDb; + if (this.heard === 0) { + if (lowDb < DROP_HEARD_DB) return; + this.short = this.long = lowDb; + } + this.heard += dt; this.short += (lowDb - this.short) * ease(dt, DROP_SHORT); - this.long += (lowDb - this.long) * ease(dt, DROP_LONG); + this.long += (lowDb - this.long) * ease(dt, Math.min(DROP_LONG, DROP_SHORT + this.heard)); const target = clamp01((this.short - this.long - DROP_RISE_DB) / DROP_FULL_DB); f.drop += (target - f.drop) * ease(dt, target > f.drop ? DROP_ATTACK : DROP_RELEASE); }