@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.
- package/dist/{chunk-KIGWMJLC.js → chunk-5WZ7TTDW.js} +1329 -26
- package/dist/chunk-5WZ7TTDW.js.map +1 -0
- package/dist/index.d.ts +2051 -87
- package/dist/index.js +3325 -20
- package/dist/index.js.map +1 -1
- package/dist/{runMir-CVEIxPd3.d.ts → runMir-Dmu75LP0.d.ts} +2 -2
- package/dist/runner/runMir.d.ts +2 -2
- package/dist/runner/runMir.js +1 -1
- package/dist/runner/workerProtocol.d.ts +1 -1
- package/dist/types-DqH4umN8.d.ts +761 -0
- package/package.json +1 -1
- package/src/dsp/activity.ts +544 -0
- package/src/dsp/bandCqt.ts +662 -0
- package/src/dsp/bandEvents.ts +351 -0
- package/src/dsp/bandMask.ts +225 -0
- package/src/dsp/bandMir.ts +524 -0
- package/src/dsp/bandProposal.ts +552 -0
- package/src/dsp/cqt.ts +386 -0
- package/src/dsp/cqtSignals.ts +462 -0
- package/src/dsp/customSignalReduction.ts +841 -0
- package/src/dsp/eventToSignal.ts +531 -0
- package/src/dsp/frequencyBand.ts +956 -0
- package/src/dsp/mel.ts +56 -3
- package/src/dsp/musicalTime.ts +240 -0
- package/src/dsp/onset.ts +296 -30
- package/src/dsp/peakPicking.ts +519 -0
- package/src/dsp/phaseAlignment.ts +153 -0
- package/src/dsp/pitch.ts +289 -0
- package/src/dsp/resample.ts +44 -0
- package/src/dsp/signalTransforms.ts +660 -0
- package/src/dsp/silenceGating.ts +511 -0
- package/src/dsp/spectral.ts +124 -4
- package/src/index.ts +345 -4
- package/src/runner/runMir.ts +162 -2
- package/src/types.ts +650 -3
- package/dist/chunk-KIGWMJLC.js.map +0 -1
- package/dist/types-4bAZI4F7.d.ts +0 -190
|
@@ -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
|
|
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 =
|
|
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] =
|
|
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 =
|
|
448
|
+
const dt = frameDurationSec;
|
|
220
449
|
const windowFrames = Math.max(1, Math.round(smoothMs / 1e3 / Math.max(1e-9, dt)));
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
|
465
|
+
return result;
|
|
227
466
|
}
|
|
228
467
|
function onsetEnvelopeFromMel(mel, options) {
|
|
229
|
-
const opts =
|
|
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] =
|
|
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 =
|
|
515
|
+
const dt = frameDurationSec;
|
|
257
516
|
const windowFrames = Math.max(1, Math.round(smoothMs / 1e3 / Math.max(1e-9, dt)));
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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-
|
|
1931
|
-
//# sourceMappingURL=chunk-
|
|
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
|