@octoseq/mir 0.1.0-main.ef9b77a → 0.1.0-main.f485112
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-OLIDGECY.js → chunk-5WZ7TTDW.js} +839 -26
- package/dist/chunk-5WZ7TTDW.js.map +1 -0
- package/dist/index.d.ts +1257 -31
- package/dist/index.js +2280 -297
- package/dist/index.js.map +1 -1
- package/dist/{runMir-8PX3FuZC.d.ts → runMir-Dmu75LP0.d.ts} +1 -1
- 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-uMUczpax.d.ts → types-DqH4umN8.d.ts} +146 -7
- package/package.json +2 -2
- package/src/dsp/activity.ts +544 -0
- package/src/dsp/bandCqt.ts +662 -0
- package/src/dsp/bandEvents.ts +351 -0
- package/src/dsp/bandMir.ts +69 -0
- package/src/dsp/bandProposal.ts +1 -0
- package/src/dsp/customSignalReduction.ts +841 -0
- package/src/dsp/eventToSignal.ts +531 -0
- package/src/dsp/frequencyBand.ts +46 -3
- package/src/dsp/onset.ts +296 -30
- package/src/dsp/peakPicking.ts +519 -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 +195 -2
- package/src/runner/runMir.ts +116 -1
- package/src/types.ts +169 -6
- package/dist/chunk-OLIDGECY.js.map +0 -1
|
@@ -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));
|
|
216
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);
|
|
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;
|
|
@@ -2013,6 +2350,395 @@ function computeAllCqtSignals(cqt) {
|
|
|
2013
2350
|
return results;
|
|
2014
2351
|
}
|
|
2015
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
|
+
|
|
2016
2742
|
// src/runner/runMir.ts
|
|
2017
2743
|
function nowMs2() {
|
|
2018
2744
|
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
@@ -2039,6 +2765,27 @@ async function runMir(audio, request, options = {}) {
|
|
|
2039
2765
|
hopSize: 512,
|
|
2040
2766
|
window: "hann"
|
|
2041
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
|
+
}
|
|
2042
2789
|
const cpuStart = nowMs2();
|
|
2043
2790
|
const spec = await spectrogram(asAudioBufferLike(audio), specConfig, void 0, {
|
|
2044
2791
|
isCancelled: options.isCancelled
|
|
@@ -2398,6 +3145,72 @@ async function runMir(audio, request, options = {}) {
|
|
|
2398
3145
|
}
|
|
2399
3146
|
};
|
|
2400
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
|
+
}
|
|
2401
3214
|
const { mel, usedGpu, gpuMs, cpuExtraMs } = await computeMel(backend === "gpu");
|
|
2402
3215
|
const end = nowMs2();
|
|
2403
3216
|
return {
|
|
@@ -2416,6 +3229,6 @@ async function runMir(audio, request, options = {}) {
|
|
|
2416
3229
|
};
|
|
2417
3230
|
}
|
|
2418
3231
|
|
|
2419
|
-
export { CQT_DEFAULTS, bassPitchMotion, beatSalienceFromMel, computeAllCqtSignals, computeCqt, computeCqtSignal, cqtBinToHz, cqtSpectrogram, delta, deltaDelta, detectBeatCandidates, featureIndexToHz, generateTempoHypotheses, getCqtBinFrequencies, getNumBins, getNumOctaves, harmonicEnergy, hpss, hzToCqtBin, hzToFeatureIndex, hzToMel, melSpectrogram, melToHz, mfcc, onsetEnvelopeFromMel, onsetEnvelopeFromMelGpu, onsetEnvelopeFromSpectrogram, peakPick, runMir, spectralCentroid, spectralFlux, spectrogram, tonalStability, withCqtDefaults };
|
|
2420
|
-
//# sourceMappingURL=chunk-
|
|
2421
|
-
//# 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
|