/** * Rate reduction and onset refinement for the classifier path, both * carried over from audio-spike-sorting so snippets cut here match the * ones the benchmark cuts. */ /** Downsample by box-averaging the input samples covered by each output * sample; a crude but adequate anti-alias for percussive material. */ export function downsampleBox(x: Float32Array, srFrom: number, srTo: number): Float32Array { if (srFrom === srTo) return x; const n = Math.floor((x.length * srTo) / srFrom); const out = new Float32Array(n); for (let i = 0; i < n; i++) { const a = Math.floor((i * srFrom) / srTo); const b = Math.max(a + 1, Math.floor(((i + 1) * srFrom) / srTo)); let s = 0; for (let j = a; j < b && j < x.length; j++) s += x[j]; out[i] = s / (b - a); } return out; } /** Refine a coarse onset to the foot of the energy rise: find the * short-term energy peak within a window after the coarse time, then * walk back to where the envelope first falls below a small fraction of * that peak. */ export function refineOnset(x: Float32Array, sr: number, tCoarse: number): number { const w = Math.max(1, Math.round(0.002 * sr)); const from = Math.max(0, Math.round((tCoarse - 0.03) * sr)); const to = Math.min(x.length - w, Math.round((tCoarse + 0.05) * sr)); if (to <= from) return Math.round(tCoarse * sr); const env = (s: number) => { let e = 0; for (let i = 0; i < w; i++) e += x[s + i] * x[s + i]; return e; }; let peakS = from, peakE = 0; for (let s = from; s <= to; s += w) { const e = env(s); if (e > peakE) { peakE = e; peakS = s; } } if (peakE === 0) return Math.round(tCoarse * sr); let s = peakS; while (s - w >= from) { const e = env(s - w); if (e < 0.02 * peakE) break; if (e > env(s)) break; // rising again: an earlier, separate bump s -= w; } return s; }