Start drop detection from the first sound and count onsets in the debug overlay

Previews often open inside a drop; the long loudness average now covers no more than
what has been heard, so that is the baseline rather than a drop.
This commit is contained in:
nvrl committed 2026-11-01 14:32:51 +01:00
1 parent 856ccb3b9a
commit e8c0f40128
2 files changed
+24 -5

No files matched your search

+14 -2
View File
@@ -5,20 +5,32 @@ import type { Features } from "./features";
const KEYS: (keyof Features)[] = ["sub", "low", "mid", "high", "kick", "snare", "wobble", "drop"]; 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 { 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"); const canvas = document.createElement("canvas");
canvas.className = "audio-debug"; 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); document.body.append(canvas);
const ctx = canvas.getContext("2d")!; const ctx = canvas.getContext("2d")!;
ctx.font = "10px system-ui, sans-serif"; ctx.font = "10px system-ui, sans-serif";
const bar = w / KEYS.length; const bar = w / KEYS.length;
return (f) => { 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.clearRect(0, 0, w, h);
ctx.fillStyle = "#fff9";
ctx.fillText(`kicks ${counts.kicks} · snares ${counts.snares}`, 4, 11);
KEYS.forEach((k, i) => { KEYS.forEach((k, i) => {
const v = Math.min(1, Math.max(0, f[k])); const v = Math.min(1, Math.max(0, f[k]));
ctx.fillStyle = k === "kick" || k === "snare" ? "#ff4d6d" : k === "drop" ? "#ffb02e" : "#7fd4ff"; 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.fillStyle = "#fff9";
ctx.fillText(k, i * bar + 3, h - 3); ctx.fillText(k, i * bar + 3, h - 3);
}); });
+10 -3
View File
@@ -43,6 +43,7 @@ const SNARE_DECAY = 0.12; // s
const WOBBLE_SMOOTH = 0.12; // s const WOBBLE_SMOOTH = 0.12; // s
const DROP_SHORT = 1; // s of low-end loudness … const DROP_SHORT = 1; // s of low-end loudness …
const DROP_LONG = 12; // … against this much 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_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_FULL_DB = 5; // dB above the start for a full drop
const DROP_ATTACK = 0.25; // s const DROP_ATTACK = 0.25; // s
@@ -103,6 +104,9 @@ export class FeatureTracker {
private wobblePeak = 1; private wobblePeak = 1;
private short = SILENCE_DB; private short = SILENCE_DB;
private long = 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. /// One frame from the analyser.
fromAnalyser(an: AnalyserNode, now: number, dt: number): Features { 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)); this.wobblePeak = Math.max(1, speed, this.wobblePeak * Math.exp(-dt / 4));
f.wobble += (clamp01(speed / this.wobblePeak) - f.wobble) * ease(dt, WOBBLE_SMOOTH); 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.heard === 0) {
if (this.long <= SILENCE_DB) this.short = this.long = lowDb; if (lowDb < DROP_HEARD_DB) return;
this.short = this.long = lowDb;
}
this.heard += dt;
this.short += (lowDb - this.short) * ease(dt, DROP_SHORT); 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); 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); f.drop += (target - f.drop) * ease(dt, target > f.drop ? DROP_ATTACK : DROP_RELEASE);
} }