@octoseq/mir 0.1.0-main.4baa7cd → 0.1.0-main.4d96254

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.
@@ -158,6 +158,212 @@ async function gpuOnsetEnvelopeFromMelFlat(gpu, input) {
158
158
  };
159
159
  }
160
160
 
161
+ // src/dsp/silenceGating.ts
162
+ var DEFAULT_SILENCE_GATE_CONFIG = {
163
+ enabled: false,
164
+ // Disabled by default to avoid breaking existing behavior
165
+ energyPercentile: 10,
166
+ enterMargin: 6,
167
+ exitMargin: 3,
168
+ hangoverMs: 50,
169
+ minActiveMs: 0,
170
+ postSilenceSuppressMs: 50
171
+ };
172
+ var DEFAULT_BIN_GATE_CONFIG = {
173
+ enabled: true,
174
+ binFloorRel: 0.05
175
+ };
176
+ function withSilenceGateDefaults(config) {
177
+ return {
178
+ enabled: config?.enabled ?? DEFAULT_SILENCE_GATE_CONFIG.enabled,
179
+ energyPercentile: config?.energyPercentile ?? DEFAULT_SILENCE_GATE_CONFIG.energyPercentile,
180
+ enterMargin: config?.enterMargin ?? DEFAULT_SILENCE_GATE_CONFIG.enterMargin,
181
+ exitMargin: config?.exitMargin ?? DEFAULT_SILENCE_GATE_CONFIG.exitMargin,
182
+ hangoverMs: config?.hangoverMs ?? DEFAULT_SILENCE_GATE_CONFIG.hangoverMs,
183
+ minActiveMs: config?.minActiveMs ?? DEFAULT_SILENCE_GATE_CONFIG.minActiveMs,
184
+ postSilenceSuppressMs: config?.postSilenceSuppressMs ?? DEFAULT_SILENCE_GATE_CONFIG.postSilenceSuppressMs
185
+ };
186
+ }
187
+ function withBinGateDefaults(config) {
188
+ return {
189
+ enabled: config?.enabled ?? DEFAULT_BIN_GATE_CONFIG.enabled,
190
+ binFloorRel: config?.binFloorRel ?? DEFAULT_BIN_GATE_CONFIG.binFloorRel
191
+ };
192
+ }
193
+ function computeFrameEnergyFromMel(melBands) {
194
+ const nFrames = melBands.length;
195
+ const energy = new Float32Array(nFrames);
196
+ for (let t = 0; t < nFrames; t++) {
197
+ const bands = melBands[t];
198
+ if (!bands || bands.length === 0) {
199
+ energy[t] = -100;
200
+ continue;
201
+ }
202
+ let sum = 0;
203
+ for (let m = 0; m < bands.length; m++) {
204
+ sum += bands[m] ?? -100;
205
+ }
206
+ energy[t] = sum / bands.length;
207
+ }
208
+ return energy;
209
+ }
210
+ function computeFrameEnergyFromSpectrogram(magnitudes, usePower = false) {
211
+ const nFrames = magnitudes.length;
212
+ const energy = new Float32Array(nFrames);
213
+ for (let t = 0; t < nFrames; t++) {
214
+ const mags = magnitudes[t];
215
+ if (!mags || mags.length === 0) {
216
+ energy[t] = 0;
217
+ continue;
218
+ }
219
+ let sum = 0;
220
+ for (let k = 0; k < mags.length; k++) {
221
+ const m = mags[k] ?? 0;
222
+ sum += usePower ? m * m : m;
223
+ }
224
+ energy[t] = sum / mags.length;
225
+ }
226
+ return energy;
227
+ }
228
+ function computePercentile(values, percentile) {
229
+ if (values.length === 0) return 0;
230
+ const sorted = Float32Array.from(values).sort((a, b) => a - b);
231
+ const p = Math.max(0, Math.min(100, percentile));
232
+ const idx = p / 100 * (sorted.length - 1);
233
+ const lower = Math.floor(idx);
234
+ const upper = Math.ceil(idx);
235
+ if (lower === upper) {
236
+ return sorted[lower] ?? 0;
237
+ }
238
+ const frac = idx - lower;
239
+ const lowVal = sorted[lower] ?? 0;
240
+ const highVal = sorted[upper] ?? 0;
241
+ return lowVal + frac * (highVal - lowVal);
242
+ }
243
+ function estimateNoiseFloor(frameEnergy, percentile) {
244
+ return computePercentile(frameEnergy, percentile);
245
+ }
246
+ function buildActivityMask(frameEnergy, enterThreshold, exitThreshold, hangoverFrames) {
247
+ const nFrames = frameEnergy.length;
248
+ const mask = new Uint8Array(nFrames);
249
+ let isActive = false;
250
+ let hangoverRemaining = 0;
251
+ for (let t = 0; t < nFrames; t++) {
252
+ const e = frameEnergy[t] ?? 0;
253
+ if (!isActive) {
254
+ if (e >= enterThreshold) {
255
+ isActive = true;
256
+ hangoverRemaining = hangoverFrames;
257
+ mask[t] = 1;
258
+ } else {
259
+ mask[t] = 0;
260
+ }
261
+ } else {
262
+ if (e >= exitThreshold) {
263
+ hangoverRemaining = hangoverFrames;
264
+ mask[t] = 1;
265
+ } else if (hangoverRemaining > 0) {
266
+ hangoverRemaining--;
267
+ mask[t] = 1;
268
+ } else {
269
+ isActive = false;
270
+ mask[t] = 0;
271
+ }
272
+ }
273
+ }
274
+ return mask;
275
+ }
276
+ function buildSuppressionMask(activityMask, suppressFrames) {
277
+ const nFrames = activityMask.length;
278
+ const suppress = new Uint8Array(nFrames);
279
+ let suppressRemaining = 0;
280
+ let wasActive = false;
281
+ for (let t = 0; t < nFrames; t++) {
282
+ const isActive = (activityMask[t] ?? 0) === 1;
283
+ if (isActive && !wasActive) {
284
+ suppressRemaining = suppressFrames;
285
+ }
286
+ if (isActive && suppressRemaining > 0) {
287
+ suppress[t] = 1;
288
+ suppressRemaining--;
289
+ } else {
290
+ suppress[t] = 0;
291
+ }
292
+ wasActive = isActive;
293
+ }
294
+ return suppress;
295
+ }
296
+ function applyMinActiveDuration(activityMask, minActiveFrames) {
297
+ if (minActiveFrames <= 1) return;
298
+ const nFrames = activityMask.length;
299
+ const regions = [];
300
+ let regionStart = -1;
301
+ for (let t = 0; t <= nFrames; t++) {
302
+ const isActive = t < nFrames && (activityMask[t] ?? 0) === 1;
303
+ if (isActive && regionStart < 0) {
304
+ regionStart = t;
305
+ } else if (!isActive && regionStart >= 0) {
306
+ regions.push({ start: regionStart, end: t });
307
+ regionStart = -1;
308
+ }
309
+ }
310
+ for (const region of regions) {
311
+ const length = region.end - region.start;
312
+ if (length < minActiveFrames) {
313
+ for (let t = region.start; t < region.end; t++) {
314
+ activityMask[t] = 0;
315
+ }
316
+ }
317
+ }
318
+ }
319
+ function computeSilenceGating(frameEnergy, frameDurationSec, config) {
320
+ const cfg = withSilenceGateDefaults(config);
321
+ const nFrames = frameEnergy.length;
322
+ if (!cfg.enabled) {
323
+ return {
324
+ frameEnergy,
325
+ noiseFloor: -Infinity,
326
+ enterThreshold: -Infinity,
327
+ exitThreshold: -Infinity,
328
+ activityMask: new Uint8Array(nFrames).fill(1),
329
+ suppressionMask: new Uint8Array(nFrames)
330
+ // All zeros = no suppression
331
+ };
332
+ }
333
+ const noiseFloor = estimateNoiseFloor(frameEnergy, cfg.energyPercentile);
334
+ const enterThreshold = noiseFloor + cfg.enterMargin;
335
+ const exitThreshold = noiseFloor + cfg.exitMargin;
336
+ const hangoverFrames = Math.max(0, Math.round(cfg.hangoverMs / 1e3 / frameDurationSec));
337
+ const suppressFrames = Math.max(0, Math.round(cfg.postSilenceSuppressMs / 1e3 / frameDurationSec));
338
+ const minActiveFrames = Math.max(0, Math.round(cfg.minActiveMs / 1e3 / frameDurationSec));
339
+ const activityMask = buildActivityMask(frameEnergy, enterThreshold, exitThreshold, hangoverFrames);
340
+ if (minActiveFrames > 1) {
341
+ applyMinActiveDuration(activityMask, minActiveFrames);
342
+ }
343
+ const suppressionMask = buildSuppressionMask(activityMask, suppressFrames);
344
+ return {
345
+ frameEnergy,
346
+ noiseFloor,
347
+ enterThreshold,
348
+ exitThreshold,
349
+ activityMask,
350
+ suppressionMask
351
+ };
352
+ }
353
+ function applySilenceGating(novelty, activityMask, suppressionMask) {
354
+ const nFrames = novelty.length;
355
+ for (let t = 0; t < nFrames; t++) {
356
+ const isActive = (activityMask[t] ?? 0) === 1;
357
+ const isSuppressed = (suppressionMask[t] ?? 0) === 1;
358
+ if (!isActive || isSuppressed) {
359
+ novelty[t] = 0;
360
+ }
361
+ }
362
+ }
363
+ function computeBinFloor(frameEnergy, binFloorRel) {
364
+ return frameEnergy * binFloorRel;
365
+ }
366
+
161
367
  // src/dsp/onset.ts
162
368
  function movingAverage(values, windowFrames) {
163
369
  if (windowFrames <= 1) return values;
@@ -178,22 +384,26 @@ function movingAverage(values, windowFrames) {
178
384
  }
179
385
  return out;
180
386
  }
181
- function defaultOptions(opts) {
387
+ function resolveOptions(opts) {
182
388
  return {
183
389
  useLog: opts?.useLog ?? false,
184
390
  smoothMs: opts?.smoothMs ?? 30,
185
- diffMethod: opts?.diffMethod ?? "rectified"
391
+ diffMethod: opts?.diffMethod ?? "rectified",
392
+ silenceGate: withSilenceGateDefaults(opts?.silenceGate),
393
+ binGate: withBinGateDefaults(opts?.binGate),
394
+ returnDiagnostics: opts?.returnDiagnostics ?? false
186
395
  };
187
396
  }
188
397
  function logCompress(x) {
189
398
  return Math.log1p(Math.max(0, x));
190
399
  }
191
400
  function onsetEnvelopeFromSpectrogram(spec, options) {
192
- const opts = defaultOptions(options);
401
+ const opts = resolveOptions(options);
193
402
  const nFrames = spec.times.length;
194
- const out = new Float32Array(nFrames);
195
403
  const nBins = (spec.fftSize >>> 1) + 1;
404
+ const out = new Float32Array(nFrames);
196
405
  out[0] = 0;
406
+ const frameEnergies = opts.binGate.enabled ? computeFrameEnergyFromSpectrogram(spec.magnitudes, false) : null;
197
407
  for (let t = 1; t < nFrames; t++) {
198
408
  const cur = spec.magnitudes[t];
199
409
  const prev = spec.magnitudes[t - 1];
@@ -201,35 +411,65 @@ function onsetEnvelopeFromSpectrogram(spec, options) {
201
411
  out[t] = 0;
202
412
  continue;
203
413
  }
414
+ const binFloor = frameEnergies && opts.binGate.enabled ? computeBinFloor(frameEnergies[t] ?? 0, opts.binGate.binFloorRel) : 0;
204
415
  let sum = 0;
416
+ let validBins = 0;
205
417
  for (let k = 0; k < nBins; k++) {
206
418
  let a = cur[k] ?? 0;
207
419
  let b = prev[k] ?? 0;
420
+ if (opts.binGate.enabled && a < binFloor && b < binFloor) {
421
+ continue;
422
+ }
208
423
  if (opts.useLog) {
209
424
  a = logCompress(a);
210
425
  b = logCompress(b);
211
426
  }
212
427
  const d = a - b;
213
428
  sum += opts.diffMethod === "abs" ? Math.abs(d) : Math.max(0, d);
429
+ validBins++;
214
430
  }
215
- out[t] = nBins > 0 ? sum / nBins : 0;
431
+ out[t] = validBins > 0 ? sum / validBins : 0;
432
+ }
433
+ const linearEnergy = computeFrameEnergyFromSpectrogram(spec.magnitudes, false);
434
+ const frameEnergy = new Float32Array(nFrames);
435
+ const eps = 1e-12;
436
+ for (let t = 0; t < nFrames; t++) {
437
+ frameEnergy[t] = Math.log10(eps + (linearEnergy[t] ?? 0));
438
+ }
439
+ const frameDurationSec = nFrames >= 2 ? (spec.times[1] ?? 0) - (spec.times[0] ?? 0) : 0.01;
440
+ const gating = computeSilenceGating(frameEnergy, frameDurationSec, opts.silenceGate);
441
+ const rawNovelty = opts.returnDiagnostics ? Float32Array.from(out) : void 0;
442
+ if (opts.silenceGate.enabled) {
443
+ applySilenceGating(out, gating.activityMask, gating.suppressionMask);
216
444
  }
445
+ let values = out;
217
446
  const smoothMs = opts.smoothMs;
218
447
  if (smoothMs > 0 && nFrames >= 2) {
219
- const dt = (spec.times[1] ?? 0) - (spec.times[0] ?? 0);
448
+ const dt = frameDurationSec;
220
449
  const windowFrames = Math.max(1, Math.round(smoothMs / 1e3 / Math.max(1e-9, dt)));
221
- return {
222
- times: spec.times,
223
- values: movingAverage(out, windowFrames | 1)
450
+ const smoothed = movingAverage(out, windowFrames | 1);
451
+ values = new Float32Array(smoothed);
452
+ }
453
+ const result = { times: spec.times, values };
454
+ if (opts.returnDiagnostics && rawNovelty) {
455
+ result.diagnostics = {
456
+ frameEnergy,
457
+ noiseFloor: gating.noiseFloor,
458
+ enterThreshold: gating.enterThreshold,
459
+ exitThreshold: gating.exitThreshold,
460
+ activityMask: gating.activityMask,
461
+ suppressionMask: gating.suppressionMask,
462
+ rawNovelty
224
463
  };
225
464
  }
226
- return { times: spec.times, values: out };
465
+ return result;
227
466
  }
228
467
  function onsetEnvelopeFromMel(mel, options) {
229
- const opts = defaultOptions(options);
468
+ const opts = resolveOptions(options);
230
469
  const nFrames = mel.times.length;
231
470
  const out = new Float32Array(nFrames);
232
471
  out[0] = 0;
472
+ const melFrameEnergies = opts.binGate.enabled ? computeFrameEnergyFromMel(mel.melBands) : null;
233
473
  for (let t = 1; t < nFrames; t++) {
234
474
  const cur = mel.melBands[t];
235
475
  const prev = mel.melBands[t - 1];
@@ -238,29 +478,58 @@ function onsetEnvelopeFromMel(mel, options) {
238
478
  continue;
239
479
  }
240
480
  const nBands = cur.length;
481
+ const frameEnergyLinear = melFrameEnergies ? Math.pow(10, melFrameEnergies[t] ?? -100) : 0;
482
+ const binFloorLinear = opts.binGate.enabled ? computeBinFloor(frameEnergyLinear, opts.binGate.binFloorRel) : 0;
241
483
  let sum = 0;
484
+ let validBands = 0;
242
485
  for (let m = 0; m < nBands; m++) {
243
486
  let a = cur[m] ?? 0;
244
487
  let b = prev[m] ?? 0;
488
+ if (opts.binGate.enabled) {
489
+ const aLinear = Math.pow(10, a);
490
+ const bLinear = Math.pow(10, b);
491
+ if (aLinear < binFloorLinear && bLinear < binFloorLinear) {
492
+ continue;
493
+ }
494
+ }
245
495
  if (opts.useLog) {
246
496
  a = logCompress(a);
247
497
  b = logCompress(b);
248
498
  }
249
499
  const d = a - b;
250
500
  sum += opts.diffMethod === "abs" ? Math.abs(d) : Math.max(0, d);
501
+ validBands++;
251
502
  }
252
- out[t] = nBands > 0 ? sum / nBands : 0;
503
+ out[t] = validBands > 0 ? sum / validBands : 0;
504
+ }
505
+ const frameEnergy = computeFrameEnergyFromMel(mel.melBands);
506
+ const frameDurationSec = nFrames >= 2 ? (mel.times[1] ?? 0) - (mel.times[0] ?? 0) : 0.01;
507
+ const gating = computeSilenceGating(frameEnergy, frameDurationSec, opts.silenceGate);
508
+ const rawNovelty = opts.returnDiagnostics ? Float32Array.from(out) : void 0;
509
+ if (opts.silenceGate.enabled) {
510
+ applySilenceGating(out, gating.activityMask, gating.suppressionMask);
253
511
  }
512
+ let values = out;
254
513
  const smoothMs = opts.smoothMs;
255
514
  if (smoothMs > 0 && nFrames >= 2) {
256
- const dt = (mel.times[1] ?? 0) - (mel.times[0] ?? 0);
515
+ const dt = frameDurationSec;
257
516
  const windowFrames = Math.max(1, Math.round(smoothMs / 1e3 / Math.max(1e-9, dt)));
258
- return {
259
- times: mel.times,
260
- values: movingAverage(out, windowFrames | 1)
517
+ const smoothed = movingAverage(out, windowFrames | 1);
518
+ values = new Float32Array(smoothed);
519
+ }
520
+ const result = { times: mel.times, values };
521
+ if (opts.returnDiagnostics && rawNovelty) {
522
+ result.diagnostics = {
523
+ frameEnergy,
524
+ noiseFloor: gating.noiseFloor,
525
+ enterThreshold: gating.enterThreshold,
526
+ exitThreshold: gating.exitThreshold,
527
+ activityMask: gating.activityMask,
528
+ suppressionMask: gating.suppressionMask,
529
+ rawNovelty
261
530
  };
262
531
  }
263
- return { times: mel.times, values: out };
532
+ return result;
264
533
  }
265
534
  async function onsetEnvelopeFromMelGpu(mel, gpu, options) {
266
535
  const nFrames = mel.times.length;
@@ -272,29 +541,87 @@ async function onsetEnvelopeFromMelGpu(mel, gpu, options) {
272
541
  melFlat.set(row, t * nMels);
273
542
  }
274
543
  const diffMethod = options?.diffMethod ?? "rectified";
544
+ const silenceGateConfig = withSilenceGateDefaults(options?.silenceGate);
545
+ const returnDiagnostics = options?.returnDiagnostics ?? false;
546
+ const smoothMs = options?.smoothMs ?? 30;
275
547
  const { value, timing } = await gpuOnsetEnvelopeFromMelFlat(gpu, {
276
548
  nFrames,
277
549
  nMels,
278
550
  melFlat,
279
551
  diffMethod
280
552
  });
281
- return {
553
+ const out = Float32Array.from(value.out);
554
+ const rawNovelty = returnDiagnostics ? Float32Array.from(out) : void 0;
555
+ const frameEnergy = computeFrameEnergyFromMel(mel.melBands);
556
+ const frameDurationSec = nFrames >= 2 ? (mel.times[1] ?? 0) - (mel.times[0] ?? 0) : 0.01;
557
+ const gating = computeSilenceGating(frameEnergy, frameDurationSec, silenceGateConfig);
558
+ if (silenceGateConfig.enabled) {
559
+ applySilenceGating(out, gating.activityMask, gating.suppressionMask);
560
+ }
561
+ let values = out;
562
+ if (smoothMs > 0 && nFrames >= 2) {
563
+ const dt = frameDurationSec;
564
+ const windowFrames = Math.max(1, Math.round(smoothMs / 1e3 / Math.max(1e-9, dt)));
565
+ const smoothed = movingAverage(out, windowFrames | 1);
566
+ values = new Float32Array(smoothed);
567
+ }
568
+ const result = {
282
569
  times: mel.times,
283
- values: value.out,
570
+ values,
284
571
  gpuTimings: { gpuSubmitToReadbackMs: timing.gpuSubmitToReadbackMs }
285
572
  };
573
+ if (returnDiagnostics && rawNovelty) {
574
+ result.diagnostics = {
575
+ frameEnergy,
576
+ noiseFloor: gating.noiseFloor,
577
+ enterThreshold: gating.enterThreshold,
578
+ exitThreshold: gating.exitThreshold,
579
+ activityMask: gating.activityMask,
580
+ suppressionMask: gating.suppressionMask,
581
+ rawNovelty
582
+ };
583
+ }
584
+ return result;
286
585
  }
287
586
 
288
587
  // src/dsp/spectral.ts
289
- function spectralCentroid(spec) {
588
+ function amplitudeEnvelope(samples, sampleRate, config) {
589
+ const hopSize = config?.hopSize ?? 512;
590
+ const windowSize = config?.windowSize ?? hopSize;
591
+ const nFrames = Math.floor((samples.length - windowSize) / hopSize) + 1;
592
+ const times = new Float32Array(nFrames);
593
+ const values = new Float32Array(nFrames);
594
+ for (let t = 0; t < nFrames; t++) {
595
+ const start = t * hopSize;
596
+ const end = Math.min(start + windowSize, samples.length);
597
+ let sumSq = 0;
598
+ for (let i = start; i < end; i++) {
599
+ const s = samples[i] ?? 0;
600
+ sumSq += s * s;
601
+ }
602
+ const rms = Math.sqrt(sumSq / (end - start));
603
+ times[t] = (start + windowSize / 2) / sampleRate;
604
+ values[t] = rms;
605
+ }
606
+ return { times, values };
607
+ }
608
+ function spectralCentroid(spec, options) {
290
609
  const nFrames = spec.times.length;
291
610
  const out = new Float32Array(nFrames);
292
611
  const nBins = (spec.fftSize >>> 1) + 1;
293
612
  const binHz = spec.sampleRate / spec.fftSize;
613
+ const activity = options?.activity;
614
+ const holdBehavior = options?.inactiveBehavior === "hold";
615
+ let lastActiveValue = 0;
294
616
  for (let t = 0; t < nFrames; t++) {
617
+ const isActive = activity ? (activity.isActive[t] ?? 0) === 1 : true;
618
+ if (!isActive) {
619
+ out[t] = holdBehavior ? lastActiveValue : 0;
620
+ continue;
621
+ }
295
622
  const mags = spec.magnitudes[t];
296
623
  if (!mags) {
297
- out[t] = 0;
624
+ out[t] = holdBehavior ? lastActiveValue : 0;
298
625
  continue;
299
626
  }
300
627
  let num = 0;
@@ -305,16 +632,26 @@ function spectralCentroid(spec) {
305
632
  num += f * m;
306
633
  den += m;
307
634
  }
308
- out[t] = den > 0 ? num / den : 0;
635
+ const centroid = den > 0 ? num / den : 0;
636
+ out[t] = centroid;
637
+ lastActiveValue = centroid;
309
638
  }
310
639
  return out;
311
640
  }
312
- function spectralFlux(spec) {
641
+ function spectralFlux(spec, options) {
313
642
  const nFrames = spec.times.length;
314
643
  const out = new Float32Array(nFrames);
315
644
  const nBins = (spec.fftSize >>> 1) + 1;
645
+ const activity = options?.activity;
316
646
  let prev = null;
317
647
  for (let t = 0; t < nFrames; t++) {
648
+ const isActive = activity ? (activity.isActive[t] ?? 0) === 1 : true;
649
+ const isSuppressed = activity ? (activity.suppressMask[t] ?? 0) === 1 : false;
650
+ if (!isActive || isSuppressed) {
651
+ out[t] = 0;
652
+ prev = null;
653
+ continue;
654
+ }
318
655
  const mags = spec.magnitudes[t];
319
656
  if (!mags) {
320
657
  out[t] = 0;
@@ -802,6 +1139,20 @@ function hzToMel(hz) {
802
1139
  function melToHz(mel) {
803
1140
  return 700 * (Math.pow(10, mel / 2595) - 1);
804
1141
  }
1142
+ function hzToFeatureIndex(hz, config) {
1143
+ const melMin = hzToMel(config.fMin);
1144
+ const melMax = hzToMel(config.fMax);
1145
+ const melHz = hzToMel(hz);
1146
+ const normalized = (melHz - melMin) / (melMax - melMin);
1147
+ return normalized * (config.nMels - 1);
1148
+ }
1149
+ function featureIndexToHz(index, config) {
1150
+ const melMin = hzToMel(config.fMin);
1151
+ const melMax = hzToMel(config.fMax);
1152
+ const normalized = index / (config.nMels - 1);
1153
+ const mel = melMin + normalized * (melMax - melMin);
1154
+ return melToHz(mel);
1155
+ }
805
1156
  function buildMelFilterBank(sampleRate, fftSize, nMels, fMin, fMax) {
806
1157
  const nBins = (fftSize >>> 1) + 1;
807
1158
  const nyquist = sampleRate / 2;
@@ -1553,6 +1904,841 @@ async function spectrogram(audio, config, gpu, options = {}) {
1553
1904
  };
1554
1905
  }
1555
1906
 
1907
+ // src/dsp/cqt.ts
1908
+ var CQT_DEFAULTS = {
1909
+ /** Quarter-tone resolution (24 bins per octave) */
1910
+ binsPerOctave: 24,
1911
+ /** C1 (lowest note on a standard piano) */
1912
+ fMin: 32.7,
1913
+ /** C9 (well above audible range for most content) */
1914
+ fMax: 8372
1915
+ };
1916
+ function cqtBinToHz(bin, config) {
1917
+ return config.fMin * Math.pow(2, bin / config.binsPerOctave);
1918
+ }
1919
+ function hzToCqtBin(hz, config) {
1920
+ if (hz <= 0) return -Infinity;
1921
+ return config.binsPerOctave * Math.log2(hz / config.fMin);
1922
+ }
1923
+ function getNumOctaves(config) {
1924
+ return Math.log2(config.fMax / config.fMin);
1925
+ }
1926
+ function getNumBins(config) {
1927
+ const nOctaves = getNumOctaves(config);
1928
+ return Math.ceil(nOctaves * config.binsPerOctave);
1929
+ }
1930
+ function getCqtBinFrequencies(config) {
1931
+ const nBins = getNumBins(config);
1932
+ const freqs = new Float32Array(nBins);
1933
+ for (let k = 0; k < nBins; k++) {
1934
+ freqs[k] = cqtBinToHz(k, config);
1935
+ }
1936
+ return freqs;
1937
+ }
1938
+ var kernelBankCache = /* @__PURE__ */ new Map();
1939
+ function kernelCacheKey(config, fftSize, sampleRate) {
1940
+ return `${config.binsPerOctave}:${config.fMin}:${config.fMax}:${fftSize}:${sampleRate}`;
1941
+ }
1942
+ function createCqtKernel(binIndex, config, fftSize, sampleRate) {
1943
+ const centerFreq = cqtBinToHz(binIndex, config);
1944
+ const freqResolution = sampleRate / fftSize;
1945
+ const Q = 1 / (Math.pow(2, 1 / config.binsPerOctave) - 1);
1946
+ const bandwidth = centerFreq / Q;
1947
+ const fLow = centerFreq - bandwidth / 2;
1948
+ const fHigh = centerFreq + bandwidth / 2;
1949
+ const startBin = Math.max(0, Math.floor(fLow / freqResolution));
1950
+ const endBin = Math.min(
1951
+ Math.floor(fftSize / 2) + 1,
1952
+ Math.ceil(fHigh / freqResolution) + 1
1953
+ );
1954
+ const numBins = Math.max(1, endBin - startBin);
1955
+ const weights = new Float32Array(numBins);
1956
+ for (let i = 0; i < numBins; i++) {
1957
+ const binFreq = (startBin + i) * freqResolution;
1958
+ if (binFreq <= centerFreq) {
1959
+ if (centerFreq > fLow) {
1960
+ weights[i] = (binFreq - fLow) / (centerFreq - fLow);
1961
+ } else {
1962
+ weights[i] = 1;
1963
+ }
1964
+ } else {
1965
+ if (fHigh > centerFreq) {
1966
+ weights[i] = (fHigh - binFreq) / (fHigh - centerFreq);
1967
+ } else {
1968
+ weights[i] = 1;
1969
+ }
1970
+ }
1971
+ weights[i] = Math.max(0, Math.min(1, weights[i] ?? 0));
1972
+ }
1973
+ let sum = 0;
1974
+ for (let i = 0; i < numBins; i++) {
1975
+ sum += weights[i] ?? 0;
1976
+ }
1977
+ if (sum > 0) {
1978
+ for (let i = 0; i < numBins; i++) {
1979
+ weights[i] = (weights[i] ?? 0) / sum;
1980
+ }
1981
+ }
1982
+ return {
1983
+ centerFreq,
1984
+ startBin,
1985
+ endBin,
1986
+ weights
1987
+ };
1988
+ }
1989
+ function getCqtKernelBank(config, fftSize, sampleRate) {
1990
+ const key = kernelCacheKey(config, fftSize, sampleRate);
1991
+ const cached = kernelBankCache.get(key);
1992
+ if (cached) return cached;
1993
+ const nBins = getNumBins(config);
1994
+ const kernels = new Array(nBins);
1995
+ for (let k = 0; k < nBins; k++) {
1996
+ kernels[k] = createCqtKernel(k, config, fftSize, sampleRate);
1997
+ }
1998
+ const bank = {
1999
+ config,
2000
+ fftSize,
2001
+ sampleRate,
2002
+ kernels
2003
+ };
2004
+ kernelBankCache.set(key, bank);
2005
+ return bank;
2006
+ }
2007
+ function applyCqtKernels(stftMagnitudes, kernelBank) {
2008
+ const nCqtBins = kernelBank.kernels.length;
2009
+ const cqtMagnitudes = new Float32Array(nCqtBins);
2010
+ for (let k = 0; k < nCqtBins; k++) {
2011
+ const kernel = kernelBank.kernels[k];
2012
+ if (!kernel) continue;
2013
+ let sum = 0;
2014
+ for (let i = 0; i < kernel.weights.length; i++) {
2015
+ const stftBin = kernel.startBin + i;
2016
+ const stftMag = stftMagnitudes[stftBin] ?? 0;
2017
+ const weight = kernel.weights[i] ?? 0;
2018
+ sum += stftMag * weight;
2019
+ }
2020
+ cqtMagnitudes[k] = sum;
2021
+ }
2022
+ return cqtMagnitudes;
2023
+ }
2024
+ function withCqtDefaults(partial) {
2025
+ return {
2026
+ binsPerOctave: partial?.binsPerOctave ?? CQT_DEFAULTS.binsPerOctave,
2027
+ fMin: partial?.fMin ?? CQT_DEFAULTS.fMin,
2028
+ fMax: partial?.fMax ?? CQT_DEFAULTS.fMax,
2029
+ hopSize: partial?.hopSize
2030
+ };
2031
+ }
2032
+ async function cqtSpectrogram(audio, config, options = {}) {
2033
+ const sampleRate = audio.sampleRate;
2034
+ if (config.fMin <= 0) {
2035
+ throw new Error("@octoseq/mir: CQT fMin must be positive");
2036
+ }
2037
+ if (config.fMax <= config.fMin) {
2038
+ throw new Error("@octoseq/mir: CQT fMax must be greater than fMin");
2039
+ }
2040
+ if (config.binsPerOctave <= 0) {
2041
+ throw new Error("@octoseq/mir: CQT binsPerOctave must be positive");
2042
+ }
2043
+ const Q = 1 / (Math.pow(2, 1 / config.binsPerOctave) - 1);
2044
+ const minFreqResolution = config.fMin / Q / 2;
2045
+ const minFftSize = Math.ceil(sampleRate / minFreqResolution);
2046
+ let fftSize = 1;
2047
+ while (fftSize < minFftSize) {
2048
+ fftSize *= 2;
2049
+ }
2050
+ fftSize = Math.min(fftSize, 16384);
2051
+ const hopSize = config.hopSize ?? Math.floor(fftSize / 4);
2052
+ const stft = await spectrogram(
2053
+ audio,
2054
+ { fftSize, hopSize, window: "hann" },
2055
+ void 0,
2056
+ { isCancelled: options.isCancelled }
2057
+ );
2058
+ const kernelBank = getCqtKernelBank(config, fftSize, sampleRate);
2059
+ const nFrames = stft.magnitudes.length;
2060
+ const cqtMagnitudes = new Array(nFrames);
2061
+ for (let frame = 0; frame < nFrames; frame++) {
2062
+ if (options.isCancelled?.()) {
2063
+ throw new Error("@octoseq/mir: cancelled");
2064
+ }
2065
+ const stftFrame = stft.magnitudes[frame];
2066
+ if (!stftFrame) continue;
2067
+ cqtMagnitudes[frame] = applyCqtKernels(stftFrame, kernelBank);
2068
+ }
2069
+ const nOctaves = getNumOctaves(config);
2070
+ getNumBins(config);
2071
+ return {
2072
+ sampleRate,
2073
+ config,
2074
+ times: stft.times,
2075
+ magnitudes: cqtMagnitudes,
2076
+ nOctaves,
2077
+ binsPerOctave: config.binsPerOctave,
2078
+ binFrequencies: getCqtBinFrequencies(config)
2079
+ };
2080
+ }
2081
+ async function computeCqt(audio, config, options = {}) {
2082
+ const startTime = performance.now();
2083
+ const fullConfig = withCqtDefaults(config);
2084
+ const cqt = await cqtSpectrogram(audio, fullConfig, options);
2085
+ const endTime = performance.now();
2086
+ return {
2087
+ cqt,
2088
+ meta: {
2089
+ backend: "cpu",
2090
+ usedGpu: false,
2091
+ timings: {
2092
+ totalMs: endTime - startTime,
2093
+ cpuMs: endTime - startTime
2094
+ }
2095
+ }
2096
+ };
2097
+ }
2098
+
2099
+ // src/dsp/cqtSignals.ts
2100
+ var BASS_MIN_HZ = 20;
2101
+ var BASS_MAX_HZ = 300;
2102
+ var TONAL_STABILITY_WINDOW_FRAMES = 20;
2103
+ var CHROMA_BINS = 12;
2104
+ function normalizeMinMax(values) {
2105
+ let min = Infinity;
2106
+ let max = -Infinity;
2107
+ for (let i = 0; i < values.length; i++) {
2108
+ const v = values[i] ?? 0;
2109
+ if (v < min) min = v;
2110
+ if (v > max) max = v;
2111
+ }
2112
+ const range = max - min;
2113
+ const result = new Float32Array(values.length);
2114
+ if (range > 0) {
2115
+ for (let i = 0; i < values.length; i++) {
2116
+ result[i] = ((values[i] ?? 0) - min) / range;
2117
+ }
2118
+ } else {
2119
+ result.fill(0.5);
2120
+ }
2121
+ return result;
2122
+ }
2123
+ function weightedCentroid(values, startIndex = 0) {
2124
+ let sumWeighted = 0;
2125
+ let sumWeights = 0;
2126
+ for (let i = 0; i < values.length; i++) {
2127
+ const weight = values[i] ?? 0;
2128
+ sumWeighted += (startIndex + i) * weight;
2129
+ sumWeights += weight;
2130
+ }
2131
+ return sumWeights > 0 ? sumWeighted / sumWeights : startIndex + values.length / 2;
2132
+ }
2133
+ function computeHarmonicEnergyFrame(frame, cqt) {
2134
+ if (frame.length === 0) return 0;
2135
+ let totalEnergy = 0;
2136
+ for (let i = 0; i < frame.length; i++) {
2137
+ const mag = frame[i] ?? 0;
2138
+ totalEnergy += mag * mag;
2139
+ }
2140
+ if (totalEnergy === 0) return 0;
2141
+ let maxMag = 0;
2142
+ let fundamentalBin = 0;
2143
+ for (let i = 0; i < frame.length; i++) {
2144
+ const mag = frame[i] ?? 0;
2145
+ if (mag > maxMag) {
2146
+ maxMag = mag;
2147
+ fundamentalBin = i;
2148
+ }
2149
+ }
2150
+ const fundamentalFreq = cqtBinToHz(fundamentalBin, cqt.config);
2151
+ let harmonicEnergy2 = 0;
2152
+ const numHarmonics = 6;
2153
+ for (let h = 1; h <= numHarmonics; h++) {
2154
+ const harmonicFreq = fundamentalFreq * h;
2155
+ const harmonicBin = Math.round(hzToCqtBin(harmonicFreq, cqt.config));
2156
+ if (harmonicBin >= 0 && harmonicBin < frame.length) {
2157
+ const mag = frame[harmonicBin] ?? 0;
2158
+ const weight = 1 / h;
2159
+ harmonicEnergy2 += mag * mag * weight;
2160
+ }
2161
+ }
2162
+ let weightSum = 0;
2163
+ for (let h = 1; h <= numHarmonics; h++) {
2164
+ weightSum += 1 / h;
2165
+ }
2166
+ harmonicEnergy2 /= weightSum;
2167
+ return Math.min(1, harmonicEnergy2 / totalEnergy);
2168
+ }
2169
+ function harmonicEnergy(cqt) {
2170
+ const startTime = performance.now();
2171
+ const nFrames = cqt.magnitudes.length;
2172
+ const values = new Float32Array(nFrames);
2173
+ for (let frame = 0; frame < nFrames; frame++) {
2174
+ const cqtFrame = cqt.magnitudes[frame];
2175
+ if (cqtFrame) {
2176
+ values[frame] = computeHarmonicEnergyFrame(cqtFrame, cqt);
2177
+ }
2178
+ }
2179
+ const normalized = normalizeMinMax(values);
2180
+ const endTime = performance.now();
2181
+ return {
2182
+ kind: "cqt1d",
2183
+ signalId: "harmonicEnergy",
2184
+ times: cqt.times,
2185
+ values: normalized,
2186
+ meta: {
2187
+ backend: "cpu",
2188
+ usedGpu: false,
2189
+ timings: {
2190
+ totalMs: endTime - startTime,
2191
+ cpuMs: endTime - startTime
2192
+ }
2193
+ }
2194
+ };
2195
+ }
2196
+ function bassPitchMotion(cqt) {
2197
+ const startTime = performance.now();
2198
+ const nFrames = cqt.magnitudes.length;
2199
+ const bassStartBin = Math.max(0, Math.floor(hzToCqtBin(BASS_MIN_HZ, cqt.config)));
2200
+ const bassEndBin = Math.min(
2201
+ cqt.magnitudes[0]?.length ?? 0,
2202
+ Math.ceil(hzToCqtBin(BASS_MAX_HZ, cqt.config))
2203
+ );
2204
+ const bassNumBins = bassEndBin - bassStartBin;
2205
+ if (bassNumBins <= 0) {
2206
+ return {
2207
+ kind: "cqt1d",
2208
+ signalId: "bassPitchMotion",
2209
+ times: cqt.times,
2210
+ values: new Float32Array(nFrames),
2211
+ meta: {
2212
+ backend: "cpu",
2213
+ usedGpu: false,
2214
+ timings: { totalMs: 0, cpuMs: 0 }
2215
+ }
2216
+ };
2217
+ }
2218
+ const centroids = new Float32Array(nFrames);
2219
+ for (let frame = 0; frame < nFrames; frame++) {
2220
+ const cqtFrame = cqt.magnitudes[frame];
2221
+ if (!cqtFrame) continue;
2222
+ const bassBins = new Float32Array(bassNumBins);
2223
+ for (let i = 0; i < bassNumBins; i++) {
2224
+ bassBins[i] = cqtFrame[bassStartBin + i] ?? 0;
2225
+ }
2226
+ centroids[frame] = weightedCentroid(bassBins, bassStartBin);
2227
+ }
2228
+ const motion = new Float32Array(nFrames);
2229
+ for (let frame = 1; frame < nFrames; frame++) {
2230
+ motion[frame] = Math.abs((centroids[frame] ?? 0) - (centroids[frame - 1] ?? 0));
2231
+ }
2232
+ motion[0] = motion[1] ?? 0;
2233
+ const normalized = normalizeMinMax(motion);
2234
+ const endTime = performance.now();
2235
+ return {
2236
+ kind: "cqt1d",
2237
+ signalId: "bassPitchMotion",
2238
+ times: cqt.times,
2239
+ values: normalized,
2240
+ meta: {
2241
+ backend: "cpu",
2242
+ usedGpu: false,
2243
+ timings: {
2244
+ totalMs: endTime - startTime,
2245
+ cpuMs: endTime - startTime
2246
+ }
2247
+ }
2248
+ };
2249
+ }
2250
+ function computeChroma(frame, binsPerOctave) {
2251
+ const chroma = new Float32Array(CHROMA_BINS);
2252
+ const binsPerSemitone = binsPerOctave / CHROMA_BINS;
2253
+ for (let i = 0; i < frame.length; i++) {
2254
+ const chromaBin = Math.floor(i % binsPerOctave / binsPerSemitone) % CHROMA_BINS;
2255
+ const mag = frame[i] ?? 0;
2256
+ chroma[chromaBin] = (chroma[chromaBin] ?? 0) + mag * mag;
2257
+ }
2258
+ let sum = 0;
2259
+ for (let i = 0; i < CHROMA_BINS; i++) {
2260
+ sum += chroma[i] ?? 0;
2261
+ }
2262
+ if (sum > 0) {
2263
+ for (let i = 0; i < CHROMA_BINS; i++) {
2264
+ chroma[i] = (chroma[i] ?? 0) / sum;
2265
+ }
2266
+ }
2267
+ return chroma;
2268
+ }
2269
+ function tonalStability(cqt) {
2270
+ const startTime = performance.now();
2271
+ const nFrames = cqt.magnitudes.length;
2272
+ const chromas = new Array(nFrames);
2273
+ for (let frame = 0; frame < nFrames; frame++) {
2274
+ const cqtFrame = cqt.magnitudes[frame];
2275
+ if (cqtFrame) {
2276
+ chromas[frame] = computeChroma(cqtFrame, cqt.binsPerOctave);
2277
+ } else {
2278
+ chromas[frame] = new Float32Array(CHROMA_BINS);
2279
+ }
2280
+ }
2281
+ const halfWindow = Math.floor(TONAL_STABILITY_WINDOW_FRAMES / 2);
2282
+ const instability = new Float32Array(nFrames);
2283
+ for (let frame = 0; frame < nFrames; frame++) {
2284
+ const windowStart = Math.max(0, frame - halfWindow);
2285
+ const windowEnd = Math.min(nFrames, frame + halfWindow + 1);
2286
+ const windowSize = windowEnd - windowStart;
2287
+ const avgChroma = new Float32Array(CHROMA_BINS);
2288
+ for (let w = windowStart; w < windowEnd; w++) {
2289
+ const chroma = chromas[w];
2290
+ if (chroma) {
2291
+ for (let c = 0; c < CHROMA_BINS; c++) {
2292
+ avgChroma[c] = (avgChroma[c] ?? 0) + (chroma[c] ?? 0);
2293
+ }
2294
+ }
2295
+ }
2296
+ for (let c = 0; c < CHROMA_BINS; c++) {
2297
+ avgChroma[c] = (avgChroma[c] ?? 0) / windowSize;
2298
+ }
2299
+ let totalVariance = 0;
2300
+ for (let w = windowStart; w < windowEnd; w++) {
2301
+ const chroma = chromas[w];
2302
+ if (chroma) {
2303
+ for (let c = 0; c < CHROMA_BINS; c++) {
2304
+ const diff = (chroma[c] ?? 0) - (avgChroma[c] ?? 0);
2305
+ totalVariance += diff * diff;
2306
+ }
2307
+ }
2308
+ }
2309
+ totalVariance /= windowSize * CHROMA_BINS;
2310
+ instability[frame] = totalVariance;
2311
+ }
2312
+ const normalizedInstability = normalizeMinMax(instability);
2313
+ const stability = new Float32Array(nFrames);
2314
+ for (let frame = 0; frame < nFrames; frame++) {
2315
+ stability[frame] = 1 - (normalizedInstability[frame] ?? 0);
2316
+ }
2317
+ const endTime = performance.now();
2318
+ return {
2319
+ kind: "cqt1d",
2320
+ signalId: "tonalStability",
2321
+ times: cqt.times,
2322
+ values: stability,
2323
+ meta: {
2324
+ backend: "cpu",
2325
+ usedGpu: false,
2326
+ timings: {
2327
+ totalMs: endTime - startTime,
2328
+ cpuMs: endTime - startTime
2329
+ }
2330
+ }
2331
+ };
2332
+ }
2333
+ function computeCqtSignal(cqt, signalId) {
2334
+ switch (signalId) {
2335
+ case "harmonicEnergy":
2336
+ return harmonicEnergy(cqt);
2337
+ case "bassPitchMotion":
2338
+ return bassPitchMotion(cqt);
2339
+ case "tonalStability":
2340
+ return tonalStability(cqt);
2341
+ default:
2342
+ throw new Error(`@octoseq/mir: unknown CQT signal ID: ${signalId}`);
2343
+ }
2344
+ }
2345
+ function computeAllCqtSignals(cqt) {
2346
+ const results = /* @__PURE__ */ new Map();
2347
+ results.set("harmonicEnergy", harmonicEnergy(cqt));
2348
+ results.set("bassPitchMotion", bassPitchMotion(cqt));
2349
+ results.set("tonalStability", tonalStability(cqt));
2350
+ return results;
2351
+ }
2352
+
2353
+ // src/dsp/activity.ts
2354
+ var DEFAULT_ACTIVITY_CONFIG = {
2355
+ energyPercentile: 10,
2356
+ enterMargin: 6,
2357
+ exitMargin: 3,
2358
+ hangoverMs: 50,
2359
+ minActiveMs: 30,
2360
+ smoothMs: 20
2361
+ };
2362
+ function withActivityDefaults(config) {
2363
+ return {
2364
+ energyPercentile: config?.energyPercentile ?? DEFAULT_ACTIVITY_CONFIG.energyPercentile,
2365
+ enterMargin: config?.enterMargin ?? DEFAULT_ACTIVITY_CONFIG.enterMargin,
2366
+ exitMargin: config?.exitMargin ?? DEFAULT_ACTIVITY_CONFIG.exitMargin,
2367
+ hangoverMs: config?.hangoverMs ?? DEFAULT_ACTIVITY_CONFIG.hangoverMs,
2368
+ minActiveMs: config?.minActiveMs ?? DEFAULT_ACTIVITY_CONFIG.minActiveMs,
2369
+ smoothMs: config?.smoothMs ?? DEFAULT_ACTIVITY_CONFIG.smoothMs
2370
+ };
2371
+ }
2372
+ function smoothSignal(values, windowFrames) {
2373
+ if (windowFrames <= 1) return values;
2374
+ const n = values.length;
2375
+ const out = new Float32Array(n);
2376
+ const half = Math.floor(windowFrames / 2);
2377
+ const prefix = new Float64Array(n + 1);
2378
+ prefix[0] = 0;
2379
+ for (let i = 0; i < n; i++) {
2380
+ prefix[i + 1] = (prefix[i] ?? 0) + (values[i] ?? 0);
2381
+ }
2382
+ for (let i = 0; i < n; i++) {
2383
+ const start = Math.max(0, i - half);
2384
+ const end = Math.min(n, i + half + 1);
2385
+ const sum = (prefix[end] ?? 0) - (prefix[start] ?? 0);
2386
+ const count = Math.max(1, end - start);
2387
+ out[i] = sum / count;
2388
+ }
2389
+ return out;
2390
+ }
2391
+ function maskToActivityLevel(mask, smoothFrames) {
2392
+ const n = mask.length;
2393
+ const level = new Float32Array(n);
2394
+ for (let i = 0; i < n; i++) {
2395
+ level[i] = mask[i] ?? 0;
2396
+ }
2397
+ if (smoothFrames > 1) {
2398
+ return smoothSignal(level, smoothFrames);
2399
+ }
2400
+ return level;
2401
+ }
2402
+ function computeActivityFromMel(mel, config) {
2403
+ const cfg = withActivityDefaults(config);
2404
+ const nFrames = mel.times.length;
2405
+ const frameEnergy = computeFrameEnergyFromMel(mel.melBands);
2406
+ const noiseFloor = estimateNoiseFloor(frameEnergy, cfg.energyPercentile);
2407
+ const enterThreshold = noiseFloor + cfg.enterMargin;
2408
+ const exitThreshold = noiseFloor + cfg.exitMargin;
2409
+ const frameDurationSec = nFrames >= 2 ? (mel.times[1] ?? 0) - (mel.times[0] ?? 0) : 0.01;
2410
+ const hangoverFrames = Math.max(0, Math.round(cfg.hangoverMs / 1e3 / frameDurationSec));
2411
+ const minActiveFrames = Math.max(0, Math.round(cfg.minActiveMs / 1e3 / frameDurationSec));
2412
+ const smoothFrames = Math.max(1, Math.round(cfg.smoothMs / 1e3 / frameDurationSec)) | 1;
2413
+ const isActive = buildActivityMask(frameEnergy, enterThreshold, exitThreshold, hangoverFrames);
2414
+ if (minActiveFrames > 1) {
2415
+ applyMinActiveDuration(isActive, minActiveFrames);
2416
+ }
2417
+ const suppressFrames = Math.max(0, Math.round(50 / 1e3 / frameDurationSec));
2418
+ const suppressMask = buildSuppressionMask(isActive, suppressFrames);
2419
+ const activityLevel = maskToActivityLevel(isActive, smoothFrames);
2420
+ return {
2421
+ times: mel.times,
2422
+ activityLevel,
2423
+ isActive,
2424
+ suppressMask,
2425
+ diagnostics: {
2426
+ frameEnergy,
2427
+ noiseFloor,
2428
+ enterThreshold,
2429
+ exitThreshold
2430
+ }
2431
+ };
2432
+ }
2433
+ function computeActivityFromSpectrogram(spec, config) {
2434
+ const cfg = withActivityDefaults(config);
2435
+ const nFrames = spec.times.length;
2436
+ const linearEnergy = computeFrameEnergyFromSpectrogram(spec.magnitudes, false);
2437
+ const frameEnergy = new Float32Array(nFrames);
2438
+ const eps = 1e-12;
2439
+ for (let t = 0; t < nFrames; t++) {
2440
+ frameEnergy[t] = Math.log10(eps + (linearEnergy[t] ?? 0));
2441
+ }
2442
+ const noiseFloor = estimateNoiseFloor(frameEnergy, cfg.energyPercentile);
2443
+ const enterThreshold = noiseFloor + cfg.enterMargin;
2444
+ const exitThreshold = noiseFloor + cfg.exitMargin;
2445
+ const frameDurationSec = nFrames >= 2 ? (spec.times[1] ?? 0) - (spec.times[0] ?? 0) : 0.01;
2446
+ const hangoverFrames = Math.max(0, Math.round(cfg.hangoverMs / 1e3 / frameDurationSec));
2447
+ const minActiveFrames = Math.max(0, Math.round(cfg.minActiveMs / 1e3 / frameDurationSec));
2448
+ const smoothFrames = Math.max(1, Math.round(cfg.smoothMs / 1e3 / frameDurationSec)) | 1;
2449
+ const isActive = buildActivityMask(frameEnergy, enterThreshold, exitThreshold, hangoverFrames);
2450
+ if (minActiveFrames > 1) {
2451
+ applyMinActiveDuration(isActive, minActiveFrames);
2452
+ }
2453
+ const suppressFrames = Math.max(0, Math.round(50 / 1e3 / frameDurationSec));
2454
+ const suppressMask = buildSuppressionMask(isActive, suppressFrames);
2455
+ const activityLevel = maskToActivityLevel(isActive, smoothFrames);
2456
+ return {
2457
+ times: spec.times,
2458
+ activityLevel,
2459
+ isActive,
2460
+ suppressMask,
2461
+ diagnostics: {
2462
+ frameEnergy,
2463
+ noiseFloor,
2464
+ enterThreshold,
2465
+ exitThreshold
2466
+ }
2467
+ };
2468
+ }
2469
+ function computeActivityFromAudio(samples, sampleRate, hopSize, windowSize, config) {
2470
+ const cfg = withActivityDefaults(config);
2471
+ const nFrames = Math.max(0, Math.floor((samples.length - windowSize) / hopSize) + 1);
2472
+ const times = new Float32Array(nFrames);
2473
+ const frameEnergy = new Float32Array(nFrames);
2474
+ const eps = 1e-12;
2475
+ for (let frame = 0; frame < nFrames; frame++) {
2476
+ const start = frame * hopSize;
2477
+ const end = Math.min(start + windowSize, samples.length);
2478
+ let sumSq = 0;
2479
+ for (let i = start; i < end; i++) {
2480
+ const s = samples[i] ?? 0;
2481
+ sumSq += s * s;
2482
+ }
2483
+ const rms = Math.sqrt(sumSq / (end - start));
2484
+ times[frame] = (start + windowSize / 2) / sampleRate;
2485
+ frameEnergy[frame] = Math.log10(eps + rms);
2486
+ }
2487
+ const noiseFloor = estimateNoiseFloor(frameEnergy, cfg.energyPercentile);
2488
+ const enterThreshold = noiseFloor + cfg.enterMargin;
2489
+ const exitThreshold = noiseFloor + cfg.exitMargin;
2490
+ const frameDurationSec = hopSize / sampleRate;
2491
+ const hangoverFrames = Math.max(0, Math.round(cfg.hangoverMs / 1e3 / frameDurationSec));
2492
+ const minActiveFrames = Math.max(0, Math.round(cfg.minActiveMs / 1e3 / frameDurationSec));
2493
+ const smoothFrames = Math.max(1, Math.round(cfg.smoothMs / 1e3 / frameDurationSec)) | 1;
2494
+ const isActive = buildActivityMask(frameEnergy, enterThreshold, exitThreshold, hangoverFrames);
2495
+ if (minActiveFrames > 1) {
2496
+ applyMinActiveDuration(isActive, minActiveFrames);
2497
+ }
2498
+ const suppressFrames = Math.max(0, Math.round(50 / 1e3 / frameDurationSec));
2499
+ const suppressMask = buildSuppressionMask(isActive, suppressFrames);
2500
+ const activityLevel = maskToActivityLevel(isActive, smoothFrames);
2501
+ return {
2502
+ times,
2503
+ activityLevel,
2504
+ isActive,
2505
+ suppressMask,
2506
+ diagnostics: {
2507
+ frameEnergy,
2508
+ noiseFloor,
2509
+ enterThreshold,
2510
+ exitThreshold
2511
+ }
2512
+ };
2513
+ }
2514
+ function applyActivityGating(values, activity, options) {
2515
+ const useBinary = options?.useBinaryMask ?? true;
2516
+ const suppressPostSilence = options?.suppressPostSilence ?? true;
2517
+ const levelThreshold = options?.levelThreshold ?? 0.5;
2518
+ const n = Math.min(values.length, activity.activityLevel.length);
2519
+ for (let i = 0; i < n; i++) {
2520
+ let shouldGate = false;
2521
+ if (useBinary) {
2522
+ shouldGate = (activity.isActive[i] ?? 0) === 0;
2523
+ } else {
2524
+ shouldGate = (activity.activityLevel[i] ?? 0) < levelThreshold;
2525
+ }
2526
+ if (suppressPostSilence && (activity.suppressMask[i] ?? 0) === 1) {
2527
+ shouldGate = true;
2528
+ }
2529
+ if (shouldGate) {
2530
+ values[i] = 0;
2531
+ }
2532
+ }
2533
+ }
2534
+ function interpolateActivity(activity, targetTimes) {
2535
+ const n = targetTimes.length;
2536
+ const activityLevel = new Float32Array(n);
2537
+ const isActive = new Uint8Array(n);
2538
+ const suppressMask = new Uint8Array(n);
2539
+ const frameEnergy = new Float32Array(n);
2540
+ const srcTimes = activity.times;
2541
+ const srcN = srcTimes.length;
2542
+ if (srcN === 0) {
2543
+ return {
2544
+ times: targetTimes,
2545
+ activityLevel,
2546
+ isActive,
2547
+ suppressMask,
2548
+ diagnostics: {
2549
+ frameEnergy,
2550
+ noiseFloor: activity.diagnostics.noiseFloor,
2551
+ enterThreshold: activity.diagnostics.enterThreshold,
2552
+ exitThreshold: activity.diagnostics.exitThreshold
2553
+ }
2554
+ };
2555
+ }
2556
+ for (let i = 0; i < n; i++) {
2557
+ const t = targetTimes[i] ?? 0;
2558
+ let lo = 0;
2559
+ let hi = srcN - 1;
2560
+ while (lo < hi) {
2561
+ const mid = lo + hi >> 1;
2562
+ if ((srcTimes[mid] ?? 0) < t) {
2563
+ lo = mid + 1;
2564
+ } else {
2565
+ hi = mid;
2566
+ }
2567
+ }
2568
+ let nearest = lo;
2569
+ if (lo > 0 && Math.abs((srcTimes[lo - 1] ?? 0) - t) < Math.abs((srcTimes[lo] ?? 0) - t)) {
2570
+ nearest = lo - 1;
2571
+ }
2572
+ activityLevel[i] = activity.activityLevel[nearest] ?? 0;
2573
+ isActive[i] = activity.isActive[nearest] ?? 0;
2574
+ suppressMask[i] = activity.suppressMask[nearest] ?? 0;
2575
+ frameEnergy[i] = activity.diagnostics.frameEnergy[nearest] ?? 0;
2576
+ }
2577
+ return {
2578
+ times: targetTimes,
2579
+ activityLevel,
2580
+ isActive,
2581
+ suppressMask,
2582
+ diagnostics: {
2583
+ frameEnergy,
2584
+ noiseFloor: activity.diagnostics.noiseFloor,
2585
+ enterThreshold: activity.diagnostics.enterThreshold,
2586
+ exitThreshold: activity.diagnostics.exitThreshold
2587
+ }
2588
+ };
2589
+ }
2590
+
2591
+ // src/dsp/pitch.ts
2592
+ function pitchF0(samples, sampleRate, config) {
2593
+ const result = yinPitchDetection(samples, sampleRate, config);
2594
+ const f0 = result.f0;
2595
+ const activityOpts = config?.activityOptions;
2596
+ if (activityOpts?.activity) {
2597
+ const activity = interpolateActivity(activityOpts.activity, result.times);
2598
+ const behavior = activityOpts.inactiveBehavior ?? "zero";
2599
+ let lastActiveValue = 0;
2600
+ for (let i = 0; i < f0.length; i++) {
2601
+ if (!activity.isActive[i]) {
2602
+ if (behavior === "zero") {
2603
+ f0[i] = 0;
2604
+ } else {
2605
+ f0[i] = lastActiveValue;
2606
+ }
2607
+ } else {
2608
+ lastActiveValue = f0[i] ?? 0;
2609
+ }
2610
+ }
2611
+ }
2612
+ return {
2613
+ times: result.times,
2614
+ values: f0
2615
+ };
2616
+ }
2617
+ function pitchConfidence(samples, sampleRate, config) {
2618
+ const result = yinPitchDetection(samples, sampleRate, config);
2619
+ const confidence = result.confidence;
2620
+ const activityOpts = config?.activityOptions;
2621
+ if (activityOpts?.activity) {
2622
+ const activity = interpolateActivity(activityOpts.activity, result.times);
2623
+ const behavior = activityOpts.inactiveBehavior ?? "zero";
2624
+ let lastActiveValue = 0;
2625
+ for (let i = 0; i < confidence.length; i++) {
2626
+ if (!activity.isActive[i]) {
2627
+ if (behavior === "zero") {
2628
+ confidence[i] = 0;
2629
+ } else {
2630
+ confidence[i] = lastActiveValue;
2631
+ }
2632
+ } else {
2633
+ lastActiveValue = confidence[i] ?? 0;
2634
+ }
2635
+ }
2636
+ }
2637
+ return {
2638
+ times: result.times,
2639
+ values: confidence
2640
+ };
2641
+ }
2642
+ function yinPitchDetection(samples, sampleRate, config) {
2643
+ const fMinHz = config?.fMinHz ?? 50;
2644
+ const fMaxHz = config?.fMaxHz ?? 1e3;
2645
+ const hopSize = config?.hopSize ?? 512;
2646
+ const windowSize = config?.windowSize ?? 2048;
2647
+ const threshold = config?.threshold ?? 0.15;
2648
+ const tauMin = Math.max(2, Math.floor(sampleRate / fMaxHz));
2649
+ const tauMax = Math.min(windowSize / 2, Math.ceil(sampleRate / fMinHz));
2650
+ const nFrames = Math.max(0, Math.floor((samples.length - windowSize) / hopSize) + 1);
2651
+ const times = new Float32Array(nFrames);
2652
+ const f0 = new Float32Array(nFrames);
2653
+ const confidence = new Float32Array(nFrames);
2654
+ const d = new Float32Array(tauMax + 1);
2655
+ const dPrime = new Float32Array(tauMax + 1);
2656
+ for (let frame = 0; frame < nFrames; frame++) {
2657
+ const start = frame * hopSize;
2658
+ const frameCenter = start + windowSize / 2;
2659
+ times[frame] = frameCenter / sampleRate;
2660
+ const frameEnd = Math.min(start + windowSize, samples.length);
2661
+ const frameLen = frameEnd - start;
2662
+ let energy = 0;
2663
+ for (let i = 0; i < frameLen; i++) {
2664
+ const s = samples[start + i] ?? 0;
2665
+ energy += s * s;
2666
+ }
2667
+ const rms = Math.sqrt(energy / frameLen);
2668
+ if (rms < 1e-6) {
2669
+ f0[frame] = 0;
2670
+ confidence[frame] = 0;
2671
+ continue;
2672
+ }
2673
+ d[0] = 0;
2674
+ for (let tau = 1; tau <= tauMax && tau < frameLen; tau++) {
2675
+ let sum = 0;
2676
+ const limit = Math.min(frameLen - tau, windowSize - tau);
2677
+ for (let j = 0; j < limit; j++) {
2678
+ const diff = (samples[start + j] ?? 0) - (samples[start + j + tau] ?? 0);
2679
+ sum += diff * diff;
2680
+ }
2681
+ d[tau] = sum;
2682
+ }
2683
+ dPrime[0] = 1;
2684
+ let runningSum = 0;
2685
+ for (let tau = 1; tau <= tauMax && tau < frameLen; tau++) {
2686
+ const dVal = d[tau] ?? 0;
2687
+ runningSum += dVal;
2688
+ if (runningSum > 0) {
2689
+ dPrime[tau] = dVal * tau / runningSum;
2690
+ } else {
2691
+ dPrime[tau] = 1;
2692
+ }
2693
+ }
2694
+ let bestTau = -1;
2695
+ let bestDPrime = 1;
2696
+ for (let tau = tauMin; tau <= tauMax && tau < frameLen - 1; tau++) {
2697
+ const dPrimeVal = dPrime[tau] ?? 1;
2698
+ if (dPrimeVal < threshold) {
2699
+ while (tau + 1 <= tauMax && tau + 1 < frameLen - 1 && (dPrime[tau + 1] ?? 1) < (dPrime[tau] ?? 1)) {
2700
+ tau++;
2701
+ }
2702
+ bestTau = tau;
2703
+ bestDPrime = dPrime[tau] ?? 1;
2704
+ break;
2705
+ }
2706
+ }
2707
+ if (bestTau < 0) {
2708
+ for (let tau = tauMin; tau <= tauMax && tau < frameLen; tau++) {
2709
+ const dPrimeVal = dPrime[tau] ?? 1;
2710
+ if (dPrimeVal < bestDPrime) {
2711
+ bestDPrime = dPrimeVal;
2712
+ bestTau = tau;
2713
+ }
2714
+ }
2715
+ }
2716
+ if (bestTau > tauMin && bestTau < tauMax - 1 && bestTau < frameLen - 1) {
2717
+ const y0 = dPrime[bestTau - 1] ?? 1;
2718
+ const y1 = dPrime[bestTau] ?? 1;
2719
+ const y2 = dPrime[bestTau + 1] ?? 1;
2720
+ const denom = 2 * (2 * y1 - y0 - y2);
2721
+ if (Math.abs(denom) > 1e-10) {
2722
+ const delta2 = (y0 - y2) / denom;
2723
+ const interpolatedTau = bestTau + delta2;
2724
+ if (interpolatedTau >= tauMin && interpolatedTau <= tauMax) {
2725
+ f0[frame] = sampleRate / interpolatedTau;
2726
+ confidence[frame] = Math.max(0, Math.min(1, 1 - bestDPrime));
2727
+ continue;
2728
+ }
2729
+ }
2730
+ }
2731
+ if (bestTau >= tauMin && bestTau <= tauMax) {
2732
+ f0[frame] = sampleRate / bestTau;
2733
+ confidence[frame] = Math.max(0, Math.min(1, 1 - bestDPrime));
2734
+ } else {
2735
+ f0[frame] = 0;
2736
+ confidence[frame] = 0;
2737
+ }
2738
+ }
2739
+ return { times, f0, confidence };
2740
+ }
2741
+
1556
2742
  // src/runner/runMir.ts
1557
2743
  function nowMs2() {
1558
2744
  return typeof performance !== "undefined" ? performance.now() : Date.now();
@@ -1579,6 +2765,27 @@ async function runMir(audio, request, options = {}) {
1579
2765
  hopSize: 512,
1580
2766
  window: "hann"
1581
2767
  };
2768
+ if (request.fn === "amplitudeEnvelope") {
2769
+ const cpuStart2 = nowMs2();
2770
+ const result = amplitudeEnvelope(audio.mono, audio.sampleRate, {
2771
+ hopSize: specConfig.hopSize,
2772
+ windowSize: specConfig.fftSize
2773
+ });
2774
+ const cpuEnd = nowMs2();
2775
+ return {
2776
+ kind: "1d",
2777
+ times: result.times,
2778
+ values: result.values,
2779
+ meta: {
2780
+ backend: "cpu",
2781
+ usedGpu: false,
2782
+ timings: {
2783
+ totalMs: cpuEnd - t0,
2784
+ cpuMs: cpuEnd - cpuStart2
2785
+ }
2786
+ }
2787
+ };
2788
+ }
1582
2789
  const cpuStart = nowMs2();
1583
2790
  const spec = await spectrogram(asAudioBufferLike(audio), specConfig, void 0, {
1584
2791
  isCancelled: options.isCancelled
@@ -1908,6 +3115,102 @@ async function runMir(audio, request, options = {}) {
1908
3115
  }
1909
3116
  };
1910
3117
  }
3118
+ if (request.fn === "cqtHarmonicEnergy" || request.fn === "cqtBassPitchMotion" || request.fn === "cqtTonalStability") {
3119
+ const cqtStart = nowMs2();
3120
+ const cqtConfig = withCqtDefaults(request.cqt);
3121
+ const cqt = await cqtSpectrogram(asAudioBufferLike(audio), cqtConfig, {
3122
+ isCancelled: options.isCancelled
3123
+ });
3124
+ const cqtEnd = nowMs2();
3125
+ let signal;
3126
+ if (request.fn === "cqtHarmonicEnergy") {
3127
+ signal = harmonicEnergy(cqt);
3128
+ } else if (request.fn === "cqtBassPitchMotion") {
3129
+ signal = bassPitchMotion(cqt);
3130
+ } else {
3131
+ signal = tonalStability(cqt);
3132
+ }
3133
+ const end2 = nowMs2();
3134
+ return {
3135
+ kind: "1d",
3136
+ times: signal.times,
3137
+ values: signal.values,
3138
+ meta: {
3139
+ backend: "cpu",
3140
+ usedGpu: false,
3141
+ timings: {
3142
+ totalMs: end2 - t0,
3143
+ cpuMs: cqtEnd - cqtStart + (end2 - cqtEnd)
3144
+ }
3145
+ }
3146
+ };
3147
+ }
3148
+ if (request.fn === "pitchF0" || request.fn === "pitchConfidence") {
3149
+ const pitchStart = nowMs2();
3150
+ const pitchConfig = {
3151
+ fMinHz: request.pitch?.fMinHz,
3152
+ fMaxHz: request.pitch?.fMaxHz,
3153
+ threshold: request.pitch?.threshold,
3154
+ hopSize: specConfig.hopSize,
3155
+ windowSize: specConfig.fftSize
3156
+ };
3157
+ const result = request.fn === "pitchF0" ? pitchF0(audio.mono, audio.sampleRate, pitchConfig) : pitchConfidence(audio.mono, audio.sampleRate, pitchConfig);
3158
+ const end2 = nowMs2();
3159
+ return {
3160
+ kind: "1d",
3161
+ times: result.times,
3162
+ values: result.values,
3163
+ meta: {
3164
+ backend: "cpu",
3165
+ usedGpu: false,
3166
+ timings: {
3167
+ totalMs: end2 - t0,
3168
+ cpuMs: end2 - pitchStart
3169
+ }
3170
+ }
3171
+ };
3172
+ }
3173
+ if (request.fn === "activity") {
3174
+ const { mel: mel2, cpuExtraMs: melCpuMs } = await computeMel(false);
3175
+ const activityStart = nowMs2();
3176
+ const activityConfig = {
3177
+ energyPercentile: request.activity?.energyPercentile,
3178
+ enterMargin: request.activity?.enterMargin,
3179
+ exitMargin: request.activity?.exitMargin,
3180
+ hangoverMs: request.activity?.hangoverMs,
3181
+ minActiveMs: request.activity?.minActiveMs,
3182
+ smoothMs: request.activity?.smoothMs
3183
+ };
3184
+ const activity = computeActivityFromMel(mel2, activityConfig);
3185
+ const end2 = nowMs2();
3186
+ let activeCount = 0;
3187
+ for (let i = 0; i < activity.isActive.length; i++) {
3188
+ if (activity.isActive[i]) activeCount++;
3189
+ }
3190
+ return {
3191
+ kind: "activity",
3192
+ times: activity.times,
3193
+ activityLevel: activity.activityLevel,
3194
+ isActive: activity.isActive,
3195
+ suppressMask: activity.suppressMask,
3196
+ meta: {
3197
+ backend: "cpu",
3198
+ usedGpu: false,
3199
+ timings: {
3200
+ totalMs: end2 - t0,
3201
+ cpuMs: cpuAfterSpec - cpuStart + melCpuMs + (end2 - activityStart)
3202
+ }
3203
+ },
3204
+ diagnostics: {
3205
+ noiseFloor: activity.diagnostics.noiseFloor,
3206
+ enterThreshold: activity.diagnostics.enterThreshold,
3207
+ exitThreshold: activity.diagnostics.exitThreshold,
3208
+ totalFrames: activity.times.length,
3209
+ activeFrames: activeCount,
3210
+ activeFraction: activity.times.length > 0 ? activeCount / activity.times.length : 0
3211
+ }
3212
+ };
3213
+ }
1911
3214
  const { mel, usedGpu, gpuMs, cpuExtraMs } = await computeMel(backend === "gpu");
1912
3215
  const end = nowMs2();
1913
3216
  return {
@@ -1926,6 +3229,6 @@ async function runMir(audio, request, options = {}) {
1926
3229
  };
1927
3230
  }
1928
3231
 
1929
- export { beatSalienceFromMel, delta, deltaDelta, detectBeatCandidates, generateTempoHypotheses, hpss, melSpectrogram, mfcc, onsetEnvelopeFromMel, onsetEnvelopeFromMelGpu, onsetEnvelopeFromSpectrogram, peakPick, runMir, spectralCentroid, spectralFlux, spectrogram };
1930
- //# sourceMappingURL=chunk-KIGWMJLC.js.map
1931
- //# sourceMappingURL=chunk-KIGWMJLC.js.map
3232
+ export { CQT_DEFAULTS, amplitudeEnvelope, applyActivityGating, bassPitchMotion, beatSalienceFromMel, buildActivityMask, computeActivityFromAudio, computeActivityFromMel, computeActivityFromSpectrogram, computeAllCqtSignals, computeCqt, computeCqtSignal, computeFrameEnergyFromMel, computeFrameEnergyFromSpectrogram, computeSilenceGating, cqtBinToHz, cqtSpectrogram, delta, deltaDelta, detectBeatCandidates, estimateNoiseFloor, featureIndexToHz, generateTempoHypotheses, getCqtBinFrequencies, getNumBins, getNumOctaves, harmonicEnergy, hpss, hzToCqtBin, hzToFeatureIndex, hzToMel, interpolateActivity, melSpectrogram, melToHz, mfcc, onsetEnvelopeFromMel, onsetEnvelopeFromMelGpu, onsetEnvelopeFromSpectrogram, peakPick, pitchConfidence, pitchF0, runMir, spectralCentroid, spectralFlux, spectrogram, tonalStability, withBinGateDefaults, withCqtDefaults, withSilenceGateDefaults };
3233
+ //# sourceMappingURL=chunk-5WZ7TTDW.js.map
3234
+ //# sourceMappingURL=chunk-5WZ7TTDW.js.map