1/**
2 * Rate reduction and onset refinement for the classifier path, both
3 * carried over from audio-spike-sorting so snippets cut here match the
4 * ones the benchmark cuts.
5 */
7/** Downsample by box-averaging the input samples covered by each output
8 * sample; a crude but adequate anti-alias for percussive material. */
9export function downsampleBox(x: Float32Array, srFrom: number, srTo: number): Float32Array {
10 if (srFrom === srTo) return x;
11 const n = Math.floor((x.length * srTo) / srFrom);
12 const out = new Float32Array(n);
13 for (let i = 0; i < n; i++) {
14 const a = Math.floor((i * srFrom) / srTo);
15 const b = Math.max(a + 1, Math.floor(((i + 1) * srFrom) / srTo));
16 let s = 0;
17 for (let j = a; j < b && j < x.length; j++) s += x[j];
18 out[i] = s / (b - a);
19 }
20 return out;
21}
23/** Refine a coarse onset to the foot of the energy rise: find the
24 * short-term energy peak within a window after the coarse time, then
25 * walk back to where the envelope first falls below a small fraction of
26 * that peak. */
27export function refineOnset(x: Float32Array, sr: number, tCoarse: number): number {
28 const w = Math.max(1, Math.round(0.002 * sr));
29 const from = Math.max(0, Math.round((tCoarse - 0.03) * sr));
30 const to = Math.min(x.length - w, Math.round((tCoarse + 0.05) * sr));
31 if (to <= from) return Math.round(tCoarse * sr);
33 const env = (s: number) => {
34 let e = 0;
35 for (let i = 0; i < w; i++) e += x[s + i] * x[s + i];
36 return e;
37 };
38 let peakS = from, peakE = 0;
39 for (let s = from; s <= to; s += w) {
40 const e = env(s);
41 if (e > peakE) { peakE = e; peakS = s; }
42 }
43 if (peakE === 0) return Math.round(tCoarse * sr);
44 let s = peakS;
45 while (s - w >= from) {
46 const e = env(s - w);
47 if (e < 0.02 * peakE) break;
48 if (e > env(s)) break; // rising again: an earlier, separate bump
49 s -= w;
50 }
51 return s;
52}