@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octoseq/mir",
|
|
3
|
-
"version": "0.1.0-main.
|
|
3
|
+
"version": "0.1.0-main.5fdb072",
|
|
4
4
|
"description": "WebGPU-accelerated Music Information Retrieval (MIR) library (skeleton)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"@types/node": "latest",
|
|
39
39
|
"eslint": "latest",
|
|
40
40
|
"tsup": "latest",
|
|
41
|
-
"typescript": "
|
|
41
|
+
"typescript": "^5",
|
|
42
42
|
"vitest": "latest"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activity Signal - First-class MIR feature for audibility detection.
|
|
3
|
+
*
|
|
4
|
+
* The Activity Signal represents whether audible content is present at each frame.
|
|
5
|
+
* It is the foundation for silence-aware MIR processing across all features.
|
|
6
|
+
*
|
|
7
|
+
* Design Principles:
|
|
8
|
+
* 1. Silence is a distinct state, not "low energy sound"
|
|
9
|
+
* 2. Activity detection is shared, not reimplemented per feature
|
|
10
|
+
* 3. Activity is computed from whole-track context (adaptive thresholds)
|
|
11
|
+
* 4. The signal is inspectable and visualizable
|
|
12
|
+
*
|
|
13
|
+
* Semantics:
|
|
14
|
+
* - activity_level: 0 = silence/inaudible, 1 = confidently audible
|
|
15
|
+
* - is_active: boolean derived from activity_level
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { MelSpectrogram } from "./mel";
|
|
19
|
+
import type { Spectrogram } from "./spectrogram";
|
|
20
|
+
import {
|
|
21
|
+
computeFrameEnergyFromMel,
|
|
22
|
+
computeFrameEnergyFromSpectrogram,
|
|
23
|
+
estimateNoiseFloor,
|
|
24
|
+
buildActivityMask,
|
|
25
|
+
buildSuppressionMask,
|
|
26
|
+
applyMinActiveDuration,
|
|
27
|
+
} from "./silenceGating";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Configuration for activity detection.
|
|
31
|
+
*/
|
|
32
|
+
export type ActivityConfig = {
|
|
33
|
+
/**
|
|
34
|
+
* Percentile (0-100) used to estimate the noise floor from frame energies.
|
|
35
|
+
* Lower values are more conservative (estimate floor from quieter frames).
|
|
36
|
+
* @default 10
|
|
37
|
+
*/
|
|
38
|
+
energyPercentile?: number;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Margin above noise floor to enter active state.
|
|
42
|
+
* For log-scale energies (mel), this is effectively dB.
|
|
43
|
+
* @default 6
|
|
44
|
+
*/
|
|
45
|
+
enterMargin?: number;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Margin above noise floor to remain in active state.
|
|
49
|
+
* Must be less than enterMargin for proper hysteresis.
|
|
50
|
+
* @default 3
|
|
51
|
+
*/
|
|
52
|
+
exitMargin?: number;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Duration in milliseconds to remain active after energy drops below exit threshold.
|
|
56
|
+
* Prevents rapid toggling near silence.
|
|
57
|
+
* @default 50
|
|
58
|
+
*/
|
|
59
|
+
hangoverMs?: number;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Minimum duration in milliseconds a region must be active.
|
|
63
|
+
* Regions shorter than this are marked inactive.
|
|
64
|
+
* @default 30
|
|
65
|
+
*/
|
|
66
|
+
minActiveMs?: number;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Smoothing window in milliseconds for activity_level output.
|
|
70
|
+
* Produces smoother transitions. 0 = no smoothing.
|
|
71
|
+
* @default 20
|
|
72
|
+
*/
|
|
73
|
+
smoothMs?: number;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Activity Signal result - the main output type.
|
|
78
|
+
*/
|
|
79
|
+
export type ActivitySignal = {
|
|
80
|
+
/** Frame times in seconds, aligned with source spectrogram/mel. */
|
|
81
|
+
times: Float32Array;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Continuous activity level in range [0, 1].
|
|
85
|
+
* 0 = silence, 1 = confidently active.
|
|
86
|
+
* Intermediate values occur at transitions and represent uncertainty.
|
|
87
|
+
*/
|
|
88
|
+
activityLevel: Float32Array;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Binary activity mask (0 = inactive, 1 = active).
|
|
92
|
+
* Derived from the underlying hysteresis state machine.
|
|
93
|
+
*/
|
|
94
|
+
isActive: Uint8Array;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Per-frame suppression mask (0 = allow, 1 = suppress).
|
|
98
|
+
* Used to suppress features immediately after silence-to-active transitions.
|
|
99
|
+
*/
|
|
100
|
+
suppressMask: Uint8Array;
|
|
101
|
+
|
|
102
|
+
/** Diagnostic information for debugging and visualization. */
|
|
103
|
+
diagnostics: ActivityDiagnostics;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Diagnostics for inspecting activity detection behavior.
|
|
108
|
+
*/
|
|
109
|
+
export type ActivityDiagnostics = {
|
|
110
|
+
/** Per-frame energy values used for detection. */
|
|
111
|
+
frameEnergy: Float32Array;
|
|
112
|
+
|
|
113
|
+
/** Estimated noise floor value. */
|
|
114
|
+
noiseFloor: number;
|
|
115
|
+
|
|
116
|
+
/** Threshold for entering active state. */
|
|
117
|
+
enterThreshold: number;
|
|
118
|
+
|
|
119
|
+
/** Threshold for exiting active state. */
|
|
120
|
+
exitThreshold: number;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Default configuration values
|
|
124
|
+
const DEFAULT_ACTIVITY_CONFIG: Required<ActivityConfig> = {
|
|
125
|
+
energyPercentile: 10,
|
|
126
|
+
enterMargin: 6,
|
|
127
|
+
exitMargin: 3,
|
|
128
|
+
hangoverMs: 50,
|
|
129
|
+
minActiveMs: 30,
|
|
130
|
+
smoothMs: 20,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Merge user config with defaults.
|
|
135
|
+
*/
|
|
136
|
+
export function withActivityDefaults(config?: ActivityConfig): Required<ActivityConfig> {
|
|
137
|
+
return {
|
|
138
|
+
energyPercentile: config?.energyPercentile ?? DEFAULT_ACTIVITY_CONFIG.energyPercentile,
|
|
139
|
+
enterMargin: config?.enterMargin ?? DEFAULT_ACTIVITY_CONFIG.enterMargin,
|
|
140
|
+
exitMargin: config?.exitMargin ?? DEFAULT_ACTIVITY_CONFIG.exitMargin,
|
|
141
|
+
hangoverMs: config?.hangoverMs ?? DEFAULT_ACTIVITY_CONFIG.hangoverMs,
|
|
142
|
+
minActiveMs: config?.minActiveMs ?? DEFAULT_ACTIVITY_CONFIG.minActiveMs,
|
|
143
|
+
smoothMs: config?.smoothMs ?? DEFAULT_ACTIVITY_CONFIG.smoothMs,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Smooth a signal using a moving average.
|
|
149
|
+
*/
|
|
150
|
+
function smoothSignal(values: Float32Array, windowFrames: number): Float32Array {
|
|
151
|
+
if (windowFrames <= 1) return values;
|
|
152
|
+
|
|
153
|
+
const n = values.length;
|
|
154
|
+
const out = new Float32Array(n);
|
|
155
|
+
const half = Math.floor(windowFrames / 2);
|
|
156
|
+
|
|
157
|
+
// Prefix sums for O(n) moving average
|
|
158
|
+
const prefix = new Float64Array(n + 1);
|
|
159
|
+
prefix[0] = 0;
|
|
160
|
+
for (let i = 0; i < n; i++) {
|
|
161
|
+
prefix[i + 1] = (prefix[i] ?? 0) + (values[i] ?? 0);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
for (let i = 0; i < n; i++) {
|
|
165
|
+
const start = Math.max(0, i - half);
|
|
166
|
+
const end = Math.min(n, i + half + 1);
|
|
167
|
+
const sum = (prefix[end] ?? 0) - (prefix[start] ?? 0);
|
|
168
|
+
const count = Math.max(1, end - start);
|
|
169
|
+
out[i] = sum / count;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Convert binary activity mask to continuous activity level.
|
|
177
|
+
*
|
|
178
|
+
* The raw mask is 0/1. We apply smoothing to create gradual transitions
|
|
179
|
+
* that better represent uncertainty at boundaries.
|
|
180
|
+
*/
|
|
181
|
+
function maskToActivityLevel(
|
|
182
|
+
mask: Uint8Array,
|
|
183
|
+
smoothFrames: number
|
|
184
|
+
): Float32Array {
|
|
185
|
+
const n = mask.length;
|
|
186
|
+
const level = new Float32Array(n);
|
|
187
|
+
|
|
188
|
+
// Convert to float
|
|
189
|
+
for (let i = 0; i < n; i++) {
|
|
190
|
+
level[i] = mask[i] ?? 0;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Apply smoothing if requested
|
|
194
|
+
if (smoothFrames > 1) {
|
|
195
|
+
return smoothSignal(level, smoothFrames);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return level;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Compute Activity Signal from mel spectrogram.
|
|
203
|
+
*
|
|
204
|
+
* This is the primary entry point for activity detection.
|
|
205
|
+
* Mel spectrograms are preferred because their log-scale energies
|
|
206
|
+
* provide more perceptually meaningful thresholds.
|
|
207
|
+
*
|
|
208
|
+
* @param mel - Mel spectrogram (log-scale energies)
|
|
209
|
+
* @param config - Activity detection configuration
|
|
210
|
+
* @returns Complete activity signal with diagnostics
|
|
211
|
+
*/
|
|
212
|
+
export function computeActivityFromMel(
|
|
213
|
+
mel: MelSpectrogram,
|
|
214
|
+
config?: ActivityConfig
|
|
215
|
+
): ActivitySignal {
|
|
216
|
+
const cfg = withActivityDefaults(config);
|
|
217
|
+
const nFrames = mel.times.length;
|
|
218
|
+
|
|
219
|
+
// Step 1: Compute frame energy (mean of log-mel bands)
|
|
220
|
+
const frameEnergy = computeFrameEnergyFromMel(mel.melBands);
|
|
221
|
+
|
|
222
|
+
// Step 2: Estimate noise floor
|
|
223
|
+
const noiseFloor = estimateNoiseFloor(frameEnergy, cfg.energyPercentile);
|
|
224
|
+
|
|
225
|
+
// Step 3: Compute thresholds
|
|
226
|
+
const enterThreshold = noiseFloor + cfg.enterMargin;
|
|
227
|
+
const exitThreshold = noiseFloor + cfg.exitMargin;
|
|
228
|
+
|
|
229
|
+
// Step 4: Convert time parameters to frames
|
|
230
|
+
const frameDurationSec = nFrames >= 2
|
|
231
|
+
? (mel.times[1] ?? 0) - (mel.times[0] ?? 0)
|
|
232
|
+
: 0.01;
|
|
233
|
+
|
|
234
|
+
const hangoverFrames = Math.max(0, Math.round(cfg.hangoverMs / 1000 / frameDurationSec));
|
|
235
|
+
const minActiveFrames = Math.max(0, Math.round(cfg.minActiveMs / 1000 / frameDurationSec));
|
|
236
|
+
const smoothFrames = Math.max(1, Math.round(cfg.smoothMs / 1000 / frameDurationSec)) | 1; // Ensure odd
|
|
237
|
+
|
|
238
|
+
// Step 5: Build activity mask with hysteresis
|
|
239
|
+
const isActive = buildActivityMask(frameEnergy, enterThreshold, exitThreshold, hangoverFrames);
|
|
240
|
+
|
|
241
|
+
// Step 6: Apply minimum active duration
|
|
242
|
+
if (minActiveFrames > 1) {
|
|
243
|
+
applyMinActiveDuration(isActive, minActiveFrames);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Step 7: Build suppression mask (for post-silence onset suppression)
|
|
247
|
+
const suppressFrames = Math.max(0, Math.round(50 / 1000 / frameDurationSec)); // Fixed 50ms
|
|
248
|
+
const suppressMask = buildSuppressionMask(isActive, suppressFrames);
|
|
249
|
+
|
|
250
|
+
// Step 8: Convert to continuous activity level
|
|
251
|
+
const activityLevel = maskToActivityLevel(isActive, smoothFrames);
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
times: mel.times,
|
|
255
|
+
activityLevel,
|
|
256
|
+
isActive,
|
|
257
|
+
suppressMask,
|
|
258
|
+
diagnostics: {
|
|
259
|
+
frameEnergy,
|
|
260
|
+
noiseFloor,
|
|
261
|
+
enterThreshold,
|
|
262
|
+
exitThreshold,
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Compute Activity Signal from linear magnitude spectrogram.
|
|
269
|
+
*
|
|
270
|
+
* This function first converts magnitudes to log scale for threshold computation.
|
|
271
|
+
*
|
|
272
|
+
* @param spec - Linear magnitude spectrogram
|
|
273
|
+
* @param config - Activity detection configuration
|
|
274
|
+
* @returns Complete activity signal with diagnostics
|
|
275
|
+
*/
|
|
276
|
+
export function computeActivityFromSpectrogram(
|
|
277
|
+
spec: Spectrogram,
|
|
278
|
+
config?: ActivityConfig
|
|
279
|
+
): ActivitySignal {
|
|
280
|
+
const cfg = withActivityDefaults(config);
|
|
281
|
+
const nFrames = spec.times.length;
|
|
282
|
+
|
|
283
|
+
// Step 1: Compute frame energy (mean magnitude) and convert to log scale
|
|
284
|
+
const linearEnergy = computeFrameEnergyFromSpectrogram(spec.magnitudes, false);
|
|
285
|
+
const frameEnergy = new Float32Array(nFrames);
|
|
286
|
+
const eps = 1e-12;
|
|
287
|
+
for (let t = 0; t < nFrames; t++) {
|
|
288
|
+
frameEnergy[t] = Math.log10(eps + (linearEnergy[t] ?? 0));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Step 2: Estimate noise floor
|
|
292
|
+
const noiseFloor = estimateNoiseFloor(frameEnergy, cfg.energyPercentile);
|
|
293
|
+
|
|
294
|
+
// Step 3: Compute thresholds
|
|
295
|
+
const enterThreshold = noiseFloor + cfg.enterMargin;
|
|
296
|
+
const exitThreshold = noiseFloor + cfg.exitMargin;
|
|
297
|
+
|
|
298
|
+
// Step 4: Convert time parameters to frames
|
|
299
|
+
const frameDurationSec = nFrames >= 2
|
|
300
|
+
? (spec.times[1] ?? 0) - (spec.times[0] ?? 0)
|
|
301
|
+
: 0.01;
|
|
302
|
+
|
|
303
|
+
const hangoverFrames = Math.max(0, Math.round(cfg.hangoverMs / 1000 / frameDurationSec));
|
|
304
|
+
const minActiveFrames = Math.max(0, Math.round(cfg.minActiveMs / 1000 / frameDurationSec));
|
|
305
|
+
const smoothFrames = Math.max(1, Math.round(cfg.smoothMs / 1000 / frameDurationSec)) | 1;
|
|
306
|
+
|
|
307
|
+
// Step 5: Build activity mask with hysteresis
|
|
308
|
+
const isActive = buildActivityMask(frameEnergy, enterThreshold, exitThreshold, hangoverFrames);
|
|
309
|
+
|
|
310
|
+
// Step 6: Apply minimum active duration
|
|
311
|
+
if (minActiveFrames > 1) {
|
|
312
|
+
applyMinActiveDuration(isActive, minActiveFrames);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Step 7: Build suppression mask
|
|
316
|
+
const suppressFrames = Math.max(0, Math.round(50 / 1000 / frameDurationSec));
|
|
317
|
+
const suppressMask = buildSuppressionMask(isActive, suppressFrames);
|
|
318
|
+
|
|
319
|
+
// Step 8: Convert to continuous activity level
|
|
320
|
+
const activityLevel = maskToActivityLevel(isActive, smoothFrames);
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
times: spec.times,
|
|
324
|
+
activityLevel,
|
|
325
|
+
isActive,
|
|
326
|
+
suppressMask,
|
|
327
|
+
diagnostics: {
|
|
328
|
+
frameEnergy,
|
|
329
|
+
noiseFloor,
|
|
330
|
+
enterThreshold,
|
|
331
|
+
exitThreshold,
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Compute Activity Signal from raw audio samples.
|
|
338
|
+
*
|
|
339
|
+
* Uses a simple RMS-based energy computation. This is useful when
|
|
340
|
+
* no spectrogram is available, but mel-based detection is preferred.
|
|
341
|
+
*
|
|
342
|
+
* @param samples - Mono audio samples
|
|
343
|
+
* @param sampleRate - Sample rate of the audio
|
|
344
|
+
* @param hopSize - Analysis hop size in samples
|
|
345
|
+
* @param windowSize - Analysis window size in samples
|
|
346
|
+
* @param config - Activity detection configuration
|
|
347
|
+
* @returns Complete activity signal with diagnostics
|
|
348
|
+
*/
|
|
349
|
+
export function computeActivityFromAudio(
|
|
350
|
+
samples: Float32Array,
|
|
351
|
+
sampleRate: number,
|
|
352
|
+
hopSize: number,
|
|
353
|
+
windowSize: number,
|
|
354
|
+
config?: ActivityConfig
|
|
355
|
+
): ActivitySignal {
|
|
356
|
+
const cfg = withActivityDefaults(config);
|
|
357
|
+
|
|
358
|
+
// Compute frame-based RMS energy
|
|
359
|
+
const nFrames = Math.max(0, Math.floor((samples.length - windowSize) / hopSize) + 1);
|
|
360
|
+
const times = new Float32Array(nFrames);
|
|
361
|
+
const frameEnergy = new Float32Array(nFrames);
|
|
362
|
+
const eps = 1e-12;
|
|
363
|
+
|
|
364
|
+
for (let frame = 0; frame < nFrames; frame++) {
|
|
365
|
+
const start = frame * hopSize;
|
|
366
|
+
const end = Math.min(start + windowSize, samples.length);
|
|
367
|
+
|
|
368
|
+
// RMS energy
|
|
369
|
+
let sumSq = 0;
|
|
370
|
+
for (let i = start; i < end; i++) {
|
|
371
|
+
const s = samples[i] ?? 0;
|
|
372
|
+
sumSq += s * s;
|
|
373
|
+
}
|
|
374
|
+
const rms = Math.sqrt(sumSq / (end - start));
|
|
375
|
+
|
|
376
|
+
times[frame] = (start + windowSize / 2) / sampleRate;
|
|
377
|
+
// Convert to log scale for threshold computation
|
|
378
|
+
frameEnergy[frame] = Math.log10(eps + rms);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Estimate noise floor and thresholds
|
|
382
|
+
const noiseFloor = estimateNoiseFloor(frameEnergy, cfg.energyPercentile);
|
|
383
|
+
const enterThreshold = noiseFloor + cfg.enterMargin;
|
|
384
|
+
const exitThreshold = noiseFloor + cfg.exitMargin;
|
|
385
|
+
|
|
386
|
+
// Convert time parameters to frames
|
|
387
|
+
const frameDurationSec = hopSize / sampleRate;
|
|
388
|
+
const hangoverFrames = Math.max(0, Math.round(cfg.hangoverMs / 1000 / frameDurationSec));
|
|
389
|
+
const minActiveFrames = Math.max(0, Math.round(cfg.minActiveMs / 1000 / frameDurationSec));
|
|
390
|
+
const smoothFrames = Math.max(1, Math.round(cfg.smoothMs / 1000 / frameDurationSec)) | 1;
|
|
391
|
+
|
|
392
|
+
// Build activity mask
|
|
393
|
+
const isActive = buildActivityMask(frameEnergy, enterThreshold, exitThreshold, hangoverFrames);
|
|
394
|
+
|
|
395
|
+
if (minActiveFrames > 1) {
|
|
396
|
+
applyMinActiveDuration(isActive, minActiveFrames);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Build suppression mask
|
|
400
|
+
const suppressFrames = Math.max(0, Math.round(50 / 1000 / frameDurationSec));
|
|
401
|
+
const suppressMask = buildSuppressionMask(isActive, suppressFrames);
|
|
402
|
+
|
|
403
|
+
// Convert to activity level
|
|
404
|
+
const activityLevel = maskToActivityLevel(isActive, smoothFrames);
|
|
405
|
+
|
|
406
|
+
return {
|
|
407
|
+
times,
|
|
408
|
+
activityLevel,
|
|
409
|
+
isActive,
|
|
410
|
+
suppressMask,
|
|
411
|
+
diagnostics: {
|
|
412
|
+
frameEnergy,
|
|
413
|
+
noiseFloor,
|
|
414
|
+
enterThreshold,
|
|
415
|
+
exitThreshold,
|
|
416
|
+
},
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Apply activity gating to a 1D signal.
|
|
422
|
+
*
|
|
423
|
+
* Zeros out values where activity is low.
|
|
424
|
+
*
|
|
425
|
+
* @param values - Signal values to gate (modified in place)
|
|
426
|
+
* @param activity - Activity signal
|
|
427
|
+
* @param options - Gating options
|
|
428
|
+
*/
|
|
429
|
+
export function applyActivityGating(
|
|
430
|
+
values: Float32Array,
|
|
431
|
+
activity: ActivitySignal,
|
|
432
|
+
options?: {
|
|
433
|
+
/** Use binary mask instead of continuous level. Default: true */
|
|
434
|
+
useBinaryMask?: boolean;
|
|
435
|
+
/** Also apply post-silence suppression. Default: true */
|
|
436
|
+
suppressPostSilence?: boolean;
|
|
437
|
+
/** Threshold for activity_level when not using binary mask. Default: 0.5 */
|
|
438
|
+
levelThreshold?: number;
|
|
439
|
+
}
|
|
440
|
+
): void {
|
|
441
|
+
const useBinary = options?.useBinaryMask ?? true;
|
|
442
|
+
const suppressPostSilence = options?.suppressPostSilence ?? true;
|
|
443
|
+
const levelThreshold = options?.levelThreshold ?? 0.5;
|
|
444
|
+
|
|
445
|
+
const n = Math.min(values.length, activity.activityLevel.length);
|
|
446
|
+
|
|
447
|
+
for (let i = 0; i < n; i++) {
|
|
448
|
+
let shouldGate = false;
|
|
449
|
+
|
|
450
|
+
if (useBinary) {
|
|
451
|
+
shouldGate = (activity.isActive[i] ?? 0) === 0;
|
|
452
|
+
} else {
|
|
453
|
+
shouldGate = (activity.activityLevel[i] ?? 0) < levelThreshold;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (suppressPostSilence && (activity.suppressMask[i] ?? 0) === 1) {
|
|
457
|
+
shouldGate = true;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (shouldGate) {
|
|
461
|
+
values[i] = 0;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Interpolate activity signal to a different time grid.
|
|
468
|
+
*
|
|
469
|
+
* Useful when you have activity computed at one resolution (e.g., mel frames)
|
|
470
|
+
* but need it at another resolution (e.g., pitch frames).
|
|
471
|
+
*
|
|
472
|
+
* @param activity - Source activity signal
|
|
473
|
+
* @param targetTimes - Target time grid
|
|
474
|
+
* @returns Activity signal interpolated to target times
|
|
475
|
+
*/
|
|
476
|
+
export function interpolateActivity(
|
|
477
|
+
activity: ActivitySignal,
|
|
478
|
+
targetTimes: Float32Array
|
|
479
|
+
): ActivitySignal {
|
|
480
|
+
const n = targetTimes.length;
|
|
481
|
+
const activityLevel = new Float32Array(n);
|
|
482
|
+
const isActive = new Uint8Array(n);
|
|
483
|
+
const suppressMask = new Uint8Array(n);
|
|
484
|
+
const frameEnergy = new Float32Array(n);
|
|
485
|
+
|
|
486
|
+
const srcTimes = activity.times;
|
|
487
|
+
const srcN = srcTimes.length;
|
|
488
|
+
|
|
489
|
+
if (srcN === 0) {
|
|
490
|
+
return {
|
|
491
|
+
times: targetTimes,
|
|
492
|
+
activityLevel,
|
|
493
|
+
isActive,
|
|
494
|
+
suppressMask,
|
|
495
|
+
diagnostics: {
|
|
496
|
+
frameEnergy,
|
|
497
|
+
noiseFloor: activity.diagnostics.noiseFloor,
|
|
498
|
+
enterThreshold: activity.diagnostics.enterThreshold,
|
|
499
|
+
exitThreshold: activity.diagnostics.exitThreshold,
|
|
500
|
+
},
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Nearest-neighbor interpolation for each target time
|
|
505
|
+
for (let i = 0; i < n; i++) {
|
|
506
|
+
const t = targetTimes[i] ?? 0;
|
|
507
|
+
|
|
508
|
+
// Binary search for nearest source frame
|
|
509
|
+
let lo = 0;
|
|
510
|
+
let hi = srcN - 1;
|
|
511
|
+
while (lo < hi) {
|
|
512
|
+
const mid = (lo + hi) >> 1;
|
|
513
|
+
if ((srcTimes[mid] ?? 0) < t) {
|
|
514
|
+
lo = mid + 1;
|
|
515
|
+
} else {
|
|
516
|
+
hi = mid;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Check if lo or lo-1 is closer
|
|
521
|
+
let nearest = lo;
|
|
522
|
+
if (lo > 0 && Math.abs((srcTimes[lo - 1] ?? 0) - t) < Math.abs((srcTimes[lo] ?? 0) - t)) {
|
|
523
|
+
nearest = lo - 1;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
activityLevel[i] = activity.activityLevel[nearest] ?? 0;
|
|
527
|
+
isActive[i] = activity.isActive[nearest] ?? 0;
|
|
528
|
+
suppressMask[i] = activity.suppressMask[nearest] ?? 0;
|
|
529
|
+
frameEnergy[i] = activity.diagnostics.frameEnergy[nearest] ?? 0;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return {
|
|
533
|
+
times: targetTimes,
|
|
534
|
+
activityLevel,
|
|
535
|
+
isActive,
|
|
536
|
+
suppressMask,
|
|
537
|
+
diagnostics: {
|
|
538
|
+
frameEnergy,
|
|
539
|
+
noiseFloor: activity.diagnostics.noiseFloor,
|
|
540
|
+
enterThreshold: activity.diagnostics.enterThreshold,
|
|
541
|
+
exitThreshold: activity.diagnostics.exitThreshold,
|
|
542
|
+
},
|
|
543
|
+
};
|
|
544
|
+
}
|