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