@octoseq/mir 0.1.0-main.4ecb074 → 0.1.0-main.5fdb072

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/chunk-HF3QHCRK.js +3234 -0
  2. package/dist/chunk-HF3QHCRK.js.map +1 -0
  3. package/dist/index.d.ts +2122 -47
  4. package/dist/index.js +3524 -39
  5. package/dist/index.js.map +1 -1
  6. package/dist/runMir-CnJQbBr8.d.ts +187 -0
  7. package/dist/runner/runMir.d.ts +2 -2
  8. package/dist/runner/runMir.js +1 -1
  9. package/dist/runner/workerProtocol.d.ts +8 -1
  10. package/dist/runner/workerProtocol.js.map +1 -1
  11. package/dist/types-DqH4umN8.d.ts +761 -0
  12. package/package.json +2 -2
  13. package/src/dsp/activity.ts +544 -0
  14. package/src/dsp/bandCqt.ts +662 -0
  15. package/src/dsp/bandEvents.ts +351 -0
  16. package/src/dsp/bandMask.ts +225 -0
  17. package/src/dsp/bandMir.ts +524 -0
  18. package/src/dsp/bandProposal.ts +552 -0
  19. package/src/dsp/beatCandidates.ts +299 -0
  20. package/src/dsp/cqt.ts +386 -0
  21. package/src/dsp/cqtSignals.ts +462 -0
  22. package/src/dsp/customSignalReduction.ts +841 -0
  23. package/src/dsp/eventToSignal.ts +531 -0
  24. package/src/dsp/frequencyBand.ts +956 -0
  25. package/src/dsp/mel.ts +56 -3
  26. package/src/dsp/musicalTime.ts +240 -0
  27. package/src/dsp/onset.ts +296 -30
  28. package/src/dsp/peakPicking.ts +519 -0
  29. package/src/dsp/phaseAlignment.ts +153 -0
  30. package/src/dsp/pitch.ts +289 -0
  31. package/src/dsp/resample.ts +44 -0
  32. package/src/dsp/signalTransforms.ts +660 -0
  33. package/src/dsp/silenceGating.ts +511 -0
  34. package/src/dsp/spectral.ts +124 -4
  35. package/src/dsp/tempoHypotheses.ts +395 -0
  36. package/src/gpu/bufferPool.ts +266 -0
  37. package/src/gpu/context.ts +30 -6
  38. package/src/gpu/helpers.ts +85 -0
  39. package/src/gpu/melProject.ts +83 -0
  40. package/src/index.ts +366 -5
  41. package/src/runner/runMir.ts +234 -2
  42. package/src/runner/workerProtocol.ts +9 -1
  43. package/src/types.ts +768 -3
  44. package/dist/chunk-DUWYCAVG.js +0 -1525
  45. package/dist/chunk-DUWYCAVG.js.map +0 -1
  46. package/dist/runMir-CSIBwNZ3.d.ts +0 -84
  47. package/dist/types-BE3py4fZ.d.ts +0 -83
package/src/dsp/onset.ts CHANGED
@@ -3,12 +3,53 @@ import { gpuOnsetEnvelopeFromMelFlat } from "../gpu/onsetEnvelope";
3
3
 
4
4
  import type { MelSpectrogram } from "./mel";
5
5
  import type { Spectrogram } from "./spectrogram";
6
+ import type { SilenceGateConfig, BinGateConfig } from "./silenceGating";
7
+ import {
8
+ computeFrameEnergyFromMel,
9
+ computeFrameEnergyFromSpectrogram,
10
+ computeSilenceGating,
11
+ applySilenceGating,
12
+ withSilenceGateDefaults,
13
+ withBinGateDefaults,
14
+ computeBinFloor,
15
+ } from "./silenceGating";
6
16
 
7
17
  export type OnsetEnvelope = {
8
18
  times: Float32Array;
9
19
  values: Float32Array;
10
20
  };
11
21
 
22
+ /**
23
+ * Extended onset envelope result with optional debugging info.
24
+ */
25
+ export type OnsetEnvelopeResult = OnsetEnvelope & {
26
+ /**
27
+ * Optional silence gating diagnostics.
28
+ * Present when silenceGate.enabled is true and returnDiagnostics is true.
29
+ */
30
+ diagnostics?: OnsetDiagnostics;
31
+ };
32
+
33
+ /**
34
+ * Diagnostics for inspecting onset detection behavior.
35
+ */
36
+ export type OnsetDiagnostics = {
37
+ /** Per-frame energy used for gating. */
38
+ frameEnergy: Float32Array;
39
+ /** Estimated noise floor. */
40
+ noiseFloor: number;
41
+ /** Threshold for entering active state. */
42
+ enterThreshold: number;
43
+ /** Threshold for exiting active state. */
44
+ exitThreshold: number;
45
+ /** Per-frame activity mask (1 = active, 0 = inactive). */
46
+ activityMask: Uint8Array;
47
+ /** Per-frame suppression mask (1 = suppressed, 0 = allowed). */
48
+ suppressionMask: Uint8Array;
49
+ /** Raw onset novelty before gating was applied. */
50
+ rawNovelty: Float32Array;
51
+ };
52
+
12
53
  export type OnsetEnvelopeOptions = {
13
54
  /** If true, log-compress magnitudes/energies before differencing. */
14
55
  useLog?: boolean;
@@ -16,6 +57,26 @@ export type OnsetEnvelopeOptions = {
16
57
  smoothMs?: number;
17
58
  /** How to convert temporal differences into novelty. */
18
59
  diffMethod?: "rectified" | "abs";
60
+
61
+ /**
62
+ * Silence-aware gating configuration.
63
+ * Suppresses false onsets in silence or near-silence regions.
64
+ * @default { enabled: true }
65
+ */
66
+ silenceGate?: SilenceGateConfig;
67
+
68
+ /**
69
+ * Bin-level gating configuration (CPU paths only).
70
+ * Ignores bins with energy below a relative threshold.
71
+ * @default { enabled: true }
72
+ */
73
+ binGate?: BinGateConfig;
74
+
75
+ /**
76
+ * If true, include diagnostics in the result for debugging.
77
+ * @default false
78
+ */
79
+ returnDiagnostics?: boolean;
19
80
  };
20
81
 
21
82
  function movingAverage(values: Float32Array, windowFrames: number): Float32Array {
@@ -45,11 +106,23 @@ function movingAverage(values: Float32Array, windowFrames: number): Float32Array
45
106
  return out;
46
107
  }
47
108
 
48
- function defaultOptions(opts?: OnsetEnvelopeOptions): Required<OnsetEnvelopeOptions> {
109
+ type ResolvedOnsetOptions = {
110
+ useLog: boolean;
111
+ smoothMs: number;
112
+ diffMethod: "rectified" | "abs";
113
+ silenceGate: Required<SilenceGateConfig>;
114
+ binGate: Required<BinGateConfig>;
115
+ returnDiagnostics: boolean;
116
+ };
117
+
118
+ function resolveOptions(opts?: OnsetEnvelopeOptions): ResolvedOnsetOptions {
49
119
  return {
50
120
  useLog: opts?.useLog ?? false,
51
121
  smoothMs: opts?.smoothMs ?? 30,
52
122
  diffMethod: opts?.diffMethod ?? "rectified",
123
+ silenceGate: withSilenceGateDefaults(opts?.silenceGate),
124
+ binGate: withBinGateDefaults(opts?.binGate),
125
+ returnDiagnostics: opts?.returnDiagnostics ?? false,
53
126
  };
54
127
  }
55
128
 
@@ -59,17 +132,34 @@ function logCompress(x: number): number {
59
132
  return Math.log1p(Math.max(0, x));
60
133
  }
61
134
 
62
- export function onsetEnvelopeFromSpectrogram(spec: Spectrogram, options?: OnsetEnvelopeOptions): OnsetEnvelope {
63
- const opts = defaultOptions(options);
135
+ /**
136
+ * Compute onset envelope from spectrogram with silence-aware gating.
137
+ *
138
+ * The pipeline:
139
+ * 1. Compute per-frame spectral flux (novelty)
140
+ * 2. Compute per-frame energy from magnitudes
141
+ * 3. Build activity mask using adaptive noise floor + hysteresis
142
+ * 4. Apply silence gating and post-silence suppression
143
+ * 5. Apply optional smoothing
144
+ */
145
+ export function onsetEnvelopeFromSpectrogram(
146
+ spec: Spectrogram,
147
+ options?: OnsetEnvelopeOptions
148
+ ): OnsetEnvelopeResult {
149
+ const opts = resolveOptions(options);
64
150
 
65
151
  const nFrames = spec.times.length;
66
- const out = new Float32Array(nFrames);
67
-
68
152
  const nBins = (spec.fftSize >>> 1) + 1;
69
153
 
70
- // First frame has no previous frame.
154
+ // Step 1: Compute raw onset novelty with optional bin gating
155
+ const out = new Float32Array(nFrames);
71
156
  out[0] = 0;
72
157
 
158
+ // Compute frame energies for bin gating (linear scale mean magnitude)
159
+ const frameEnergies = opts.binGate.enabled
160
+ ? computeFrameEnergyFromSpectrogram(spec.magnitudes, false)
161
+ : null;
162
+
73
163
  for (let t = 1; t < nFrames; t++) {
74
164
  const cur = spec.magnitudes[t];
75
165
  const prev = spec.magnitudes[t - 1];
@@ -78,44 +168,115 @@ export function onsetEnvelopeFromSpectrogram(spec: Spectrogram, options?: OnsetE
78
168
  continue;
79
169
  }
80
170
 
171
+ // Compute bin floor for this frame if bin gating enabled
172
+ const binFloor = frameEnergies && opts.binGate.enabled
173
+ ? computeBinFloor(frameEnergies[t] ?? 0, opts.binGate.binFloorRel)
174
+ : 0;
175
+
81
176
  let sum = 0;
177
+ let validBins = 0;
178
+
82
179
  for (let k = 0; k < nBins; k++) {
83
180
  let a = cur[k] ?? 0;
84
181
  let b = prev[k] ?? 0;
182
+
183
+ // Bin-level gating: skip bins below the relative floor
184
+ if (opts.binGate.enabled && a < binFloor && b < binFloor) {
185
+ continue;
186
+ }
187
+
85
188
  if (opts.useLog) {
86
189
  a = logCompress(a);
87
190
  b = logCompress(b);
88
191
  }
89
192
  const d = a - b;
90
193
  sum += opts.diffMethod === "abs" ? Math.abs(d) : Math.max(0, d);
194
+ validBins++;
91
195
  }
92
196
 
93
- // Use an average over frequency bins so the overall scale is not tied to FFT size.
94
- out[t] = nBins > 0 ? sum / nBins : 0;
197
+ // Average over valid bins (or all bins if none were valid)
198
+ out[t] = validBins > 0 ? sum / validBins : 0;
199
+ }
200
+
201
+ // Step 2: Compute frame energy for silence gating
202
+ // For spectrograms, convert linear magnitudes to log scale for gating
203
+ // (more perceptually relevant threshold behavior)
204
+ const linearEnergy = computeFrameEnergyFromSpectrogram(spec.magnitudes, false);
205
+ const frameEnergy = new Float32Array(nFrames);
206
+ const eps = 1e-12;
207
+ for (let t = 0; t < nFrames; t++) {
208
+ frameEnergy[t] = Math.log10(eps + (linearEnergy[t] ?? 0));
209
+ }
210
+
211
+ // Step 3: Build silence gating masks
212
+ const frameDurationSec = nFrames >= 2
213
+ ? (spec.times[1] ?? 0) - (spec.times[0] ?? 0)
214
+ : 0.01; // Default if not enough frames
215
+
216
+ const gating = computeSilenceGating(frameEnergy, frameDurationSec, opts.silenceGate);
217
+
218
+ // Store raw novelty for diagnostics before gating
219
+ const rawNovelty = opts.returnDiagnostics ? Float32Array.from(out) : undefined;
220
+
221
+ // Step 4: Apply silence gating
222
+ if (opts.silenceGate.enabled) {
223
+ applySilenceGating(out, gating.activityMask, gating.suppressionMask);
95
224
  }
96
225
 
97
- // Optional smoothing based on average frame spacing.
226
+ // Step 5: Optional smoothing
227
+ let values: Float32Array = out;
98
228
  const smoothMs = opts.smoothMs;
99
229
  if (smoothMs > 0 && nFrames >= 2) {
100
- const dt = (spec.times[1] ?? 0) - (spec.times[0] ?? 0);
230
+ const dt = frameDurationSec;
101
231
  const windowFrames = Math.max(1, Math.round((smoothMs / 1000) / Math.max(1e-9, dt)));
102
- return {
103
- times: spec.times,
104
- values: movingAverage(out, windowFrames | 1),
232
+ const smoothed = movingAverage(out, windowFrames | 1);
233
+ values = new Float32Array(smoothed);
234
+ }
235
+
236
+ const result: OnsetEnvelopeResult = { times: spec.times, values };
237
+
238
+ if (opts.returnDiagnostics && rawNovelty) {
239
+ result.diagnostics = {
240
+ frameEnergy,
241
+ noiseFloor: gating.noiseFloor,
242
+ enterThreshold: gating.enterThreshold,
243
+ exitThreshold: gating.exitThreshold,
244
+ activityMask: gating.activityMask,
245
+ suppressionMask: gating.suppressionMask,
246
+ rawNovelty,
105
247
  };
106
248
  }
107
249
 
108
- return { times: spec.times, values: out };
250
+ return result;
109
251
  }
110
252
 
111
- export function onsetEnvelopeFromMel(mel: MelSpectrogram, options?: OnsetEnvelopeOptions): OnsetEnvelope {
112
- const opts = defaultOptions(options);
253
+ /**
254
+ * Compute onset envelope from mel spectrogram with silence-aware gating.
255
+ *
256
+ * The pipeline:
257
+ * 1. Compute per-frame mel flux (novelty)
258
+ * 2. Compute per-frame energy from mel bands (already log scale)
259
+ * 3. Build activity mask using adaptive noise floor + hysteresis
260
+ * 4. Apply silence gating and post-silence suppression
261
+ * 5. Apply optional smoothing
262
+ */
263
+ export function onsetEnvelopeFromMel(
264
+ mel: MelSpectrogram,
265
+ options?: OnsetEnvelopeOptions
266
+ ): OnsetEnvelopeResult {
267
+ const opts = resolveOptions(options);
113
268
 
114
269
  const nFrames = mel.times.length;
115
270
  const out = new Float32Array(nFrames);
116
271
 
117
272
  out[0] = 0;
118
273
 
274
+ // Compute frame energies for bin gating
275
+ // For mel, bands are already log10 scale, so we can use them directly
276
+ const melFrameEnergies = opts.binGate.enabled
277
+ ? computeFrameEnergyFromMel(mel.melBands)
278
+ : null;
279
+
119
280
  for (let t = 1; t < nFrames; t++) {
120
281
  const cur = mel.melBands[t];
121
282
  const prev = mel.melBands[t - 1];
@@ -126,11 +287,31 @@ export function onsetEnvelopeFromMel(mel: MelSpectrogram, options?: OnsetEnvelop
126
287
 
127
288
  const nBands = cur.length;
128
289
 
290
+ // For mel-based bin gating, use 10^(energy) to get linear scale for floor comparison
291
+ // Since mel bands are log10 values, binFloorRel makes more sense in linear space
292
+ const frameEnergyLinear = melFrameEnergies
293
+ ? Math.pow(10, melFrameEnergies[t] ?? -100)
294
+ : 0;
295
+ const binFloorLinear = opts.binGate.enabled
296
+ ? computeBinFloor(frameEnergyLinear, opts.binGate.binFloorRel)
297
+ : 0;
298
+
129
299
  let sum = 0;
300
+ let validBands = 0;
301
+
130
302
  for (let m = 0; m < nBands; m++) {
131
303
  let a = cur[m] ?? 0;
132
304
  let b = prev[m] ?? 0;
133
305
 
306
+ // For mel, values are log10 scale. Convert to linear for bin gating comparison
307
+ if (opts.binGate.enabled) {
308
+ const aLinear = Math.pow(10, a);
309
+ const bLinear = Math.pow(10, b);
310
+ if (aLinear < binFloorLinear && bLinear < binFloorLinear) {
311
+ continue;
312
+ }
313
+ }
314
+
134
315
  // Note: melSpectrogram currently outputs log10(eps + energy).
135
316
  // If useLog is requested, we apply an additional stable compression.
136
317
  if (opts.useLog) {
@@ -140,43 +321,80 @@ export function onsetEnvelopeFromMel(mel: MelSpectrogram, options?: OnsetEnvelop
140
321
 
141
322
  const d = a - b;
142
323
  sum += opts.diffMethod === "abs" ? Math.abs(d) : Math.max(0, d);
324
+ validBands++;
143
325
  }
144
326
 
145
- // Use an average over bands so the overall scale is not tied to nMels.
146
- out[t] = nBands > 0 ? sum / nBands : 0;
327
+ // Average over valid bands
328
+ out[t] = validBands > 0 ? sum / validBands : 0;
147
329
  }
148
330
 
331
+ // Step 2: Compute frame energy for silence gating
332
+ // Mel bands are already log10 scale - perfect for gating thresholds
333
+ const frameEnergy = computeFrameEnergyFromMel(mel.melBands);
334
+
335
+ // Step 3: Build silence gating masks
336
+ const frameDurationSec = nFrames >= 2
337
+ ? (mel.times[1] ?? 0) - (mel.times[0] ?? 0)
338
+ : 0.01;
339
+
340
+ const gating = computeSilenceGating(frameEnergy, frameDurationSec, opts.silenceGate);
341
+
342
+ // Store raw novelty for diagnostics
343
+ const rawNovelty = opts.returnDiagnostics ? Float32Array.from(out) : undefined;
344
+
345
+ // Step 4: Apply silence gating
346
+ if (opts.silenceGate.enabled) {
347
+ applySilenceGating(out, gating.activityMask, gating.suppressionMask);
348
+ }
349
+
350
+ // Step 5: Optional smoothing
351
+ let values: Float32Array = out;
149
352
  const smoothMs = opts.smoothMs;
150
353
  if (smoothMs > 0 && nFrames >= 2) {
151
- const dt = (mel.times[1] ?? 0) - (mel.times[0] ?? 0);
354
+ const dt = frameDurationSec;
152
355
  const windowFrames = Math.max(1, Math.round((smoothMs / 1000) / Math.max(1e-9, dt)));
153
- return {
154
- times: mel.times,
155
- values: movingAverage(out, windowFrames | 1),
356
+ const smoothed = movingAverage(out, windowFrames | 1);
357
+ values = new Float32Array(smoothed);
358
+ }
359
+
360
+ const result: OnsetEnvelopeResult = { times: mel.times, values };
361
+
362
+ if (opts.returnDiagnostics && rawNovelty) {
363
+ result.diagnostics = {
364
+ frameEnergy,
365
+ noiseFloor: gating.noiseFloor,
366
+ enterThreshold: gating.enterThreshold,
367
+ exitThreshold: gating.exitThreshold,
368
+ activityMask: gating.activityMask,
369
+ suppressionMask: gating.suppressionMask,
370
+ rawNovelty,
156
371
  };
157
372
  }
158
373
 
159
- return { times: mel.times, values: out };
374
+ return result;
160
375
  }
161
376
 
162
377
  export type OnsetEnvelopeGpuResult = {
163
378
  times: Float32Array;
164
379
  values: Float32Array;
165
380
  gpuTimings: { gpuSubmitToReadbackMs: number };
381
+ /** Optional silence gating diagnostics. */
382
+ diagnostics?: OnsetDiagnostics;
166
383
  };
167
384
 
168
385
  /**
169
- * GPU-accelerated onset envelope from mel spectrogram.
386
+ * GPU-accelerated onset envelope from mel spectrogram with silence-aware gating.
170
387
  *
171
388
  * Notes:
172
- * - This bypasses JS loops for the diff+reduction step.
173
- * - Smoothing/log options are intentionally limited for v0.1 (keeps WGSL simple).
174
- * - Callers should fall back to CPU on errors.
389
+ * - GPU kernel handles diff+reduction only (keeps WGSL simple)
390
+ * - Silence gating is applied on CPU after GPU readback
391
+ * - CPU and GPU paths produce comparable results
392
+ * - Callers should fall back to CPU on errors
175
393
  */
176
394
  export async function onsetEnvelopeFromMelGpu(
177
395
  mel: MelSpectrogram,
178
396
  gpu: MirGPU,
179
- options?: Pick<OnsetEnvelopeOptions, "diffMethod">
397
+ options?: Pick<OnsetEnvelopeOptions, "diffMethod" | "silenceGate" | "returnDiagnostics" | "smoothMs">
180
398
  ): Promise<OnsetEnvelopeGpuResult> {
181
399
  const nFrames = mel.times.length;
182
400
  const nMels = mel.melBands[0]?.length ?? 0;
@@ -189,7 +407,11 @@ export async function onsetEnvelopeFromMelGpu(
189
407
  }
190
408
 
191
409
  const diffMethod = options?.diffMethod ?? "rectified";
410
+ const silenceGateConfig = withSilenceGateDefaults(options?.silenceGate);
411
+ const returnDiagnostics = options?.returnDiagnostics ?? false;
412
+ const smoothMs = options?.smoothMs ?? 30;
192
413
 
414
+ // Run GPU kernel for diff + reduction
193
415
  const { value, timing } = await gpuOnsetEnvelopeFromMelFlat(gpu, {
194
416
  nFrames,
195
417
  nMels,
@@ -197,9 +419,53 @@ export async function onsetEnvelopeFromMelGpu(
197
419
  diffMethod,
198
420
  });
199
421
 
200
- return {
422
+ // Copy GPU output to allow modification
423
+ const out = Float32Array.from(value.out);
424
+
425
+ // Store raw novelty for diagnostics before gating
426
+ const rawNovelty = returnDiagnostics ? Float32Array.from(out) : undefined;
427
+
428
+ // Apply silence gating on CPU
429
+ const frameEnergy = computeFrameEnergyFromMel(mel.melBands);
430
+ const frameDurationSec = nFrames >= 2
431
+ ? (mel.times[1] ?? 0) - (mel.times[0] ?? 0)
432
+ : 0.01;
433
+
434
+ const gating = computeSilenceGating(frameEnergy, frameDurationSec, silenceGateConfig);
435
+
436
+ if (silenceGateConfig.enabled) {
437
+ applySilenceGating(out, gating.activityMask, gating.suppressionMask);
438
+ }
439
+
440
+ // Apply optional smoothing
441
+ let values: Float32Array = out;
442
+ if (smoothMs > 0 && nFrames >= 2) {
443
+ const dt = frameDurationSec;
444
+ const windowFrames = Math.max(1, Math.round((smoothMs / 1000) / Math.max(1e-9, dt)));
445
+ const smoothed = movingAverage(out, windowFrames | 1);
446
+ values = new Float32Array(smoothed);
447
+ }
448
+
449
+ const result: OnsetEnvelopeGpuResult = {
201
450
  times: mel.times,
202
- values: value.out,
451
+ values,
203
452
  gpuTimings: { gpuSubmitToReadbackMs: timing.gpuSubmitToReadbackMs },
204
453
  };
454
+
455
+ if (returnDiagnostics && rawNovelty) {
456
+ result.diagnostics = {
457
+ frameEnergy,
458
+ noiseFloor: gating.noiseFloor,
459
+ enterThreshold: gating.enterThreshold,
460
+ exitThreshold: gating.exitThreshold,
461
+ activityMask: gating.activityMask,
462
+ suppressionMask: gating.suppressionMask,
463
+ rawNovelty,
464
+ };
465
+ }
466
+
467
+ return result;
205
468
  }
469
+
470
+ // Re-export types for convenience
471
+ export type { SilenceGateConfig, BinGateConfig, SilenceGateResult } from "./silenceGating";