@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
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Band Event Extraction for F3.
|
|
3
|
+
*
|
|
4
|
+
* These functions extract discrete events (onset peaks, beat candidates)
|
|
5
|
+
* from band-scoped 1D signals.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
BandMir1DResult,
|
|
10
|
+
BandEventFunctionId,
|
|
11
|
+
BandEventsResult,
|
|
12
|
+
BandMirEvent,
|
|
13
|
+
BandEventDiagnostics,
|
|
14
|
+
MirRunMeta,
|
|
15
|
+
MirRunTimings,
|
|
16
|
+
} from "../types";
|
|
17
|
+
import { peakPick, type PeakPickOptions } from "./peakPick";
|
|
18
|
+
|
|
19
|
+
// Re-export types for convenience
|
|
20
|
+
export type { BandEventFunctionId, BandEventsResult, BandMirEvent, BandEventDiagnostics };
|
|
21
|
+
|
|
22
|
+
// ----------------------------
|
|
23
|
+
// Options
|
|
24
|
+
// ----------------------------
|
|
25
|
+
|
|
26
|
+
export type BandOnsetPeaksOptions = {
|
|
27
|
+
/** Minimum inter-peak interval in seconds. Default: 0.0625 (~0.125 beats at 120 BPM). */
|
|
28
|
+
minIntervalSec?: number;
|
|
29
|
+
/** Adaptive threshold factor. Default: 0.8 (conservative for bands). */
|
|
30
|
+
adaptiveFactor?: number;
|
|
31
|
+
/** Use strict peak detection (> neighbors). Default: true. */
|
|
32
|
+
strict?: boolean;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type BandBeatCandidatesOptions = {
|
|
36
|
+
/** Minimum inter-candidate interval in seconds. Default: 0.1. */
|
|
37
|
+
minIntervalSec?: number;
|
|
38
|
+
/** Threshold factor for adaptive detection. Lower = more candidates. Default: 0.5. */
|
|
39
|
+
thresholdFactor?: number;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// ----------------------------
|
|
43
|
+
// Internal Helpers
|
|
44
|
+
// ----------------------------
|
|
45
|
+
|
|
46
|
+
function nowMs(): number {
|
|
47
|
+
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function createMeta(startMs: number): MirRunMeta {
|
|
51
|
+
const endMs = nowMs();
|
|
52
|
+
const timings: MirRunTimings = {
|
|
53
|
+
totalMs: endMs - startMs,
|
|
54
|
+
cpuMs: endMs - startMs,
|
|
55
|
+
gpuMs: 0,
|
|
56
|
+
};
|
|
57
|
+
return {
|
|
58
|
+
backend: "cpu",
|
|
59
|
+
usedGpu: false,
|
|
60
|
+
timings,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function computeEventDiagnostics(
|
|
65
|
+
events: BandMirEvent[],
|
|
66
|
+
durationSec: number
|
|
67
|
+
): BandEventDiagnostics {
|
|
68
|
+
const eventCount = events.length;
|
|
69
|
+
const eventsPerSecond = durationSec > 0 ? eventCount / durationSec : 0;
|
|
70
|
+
const warnings: string[] = [];
|
|
71
|
+
|
|
72
|
+
// Check for sparse or dense event streams
|
|
73
|
+
if (durationSec > 1 && eventCount === 0) {
|
|
74
|
+
warnings.push("No events detected - signal may be too quiet or noisy");
|
|
75
|
+
} else if (eventsPerSecond > 20) {
|
|
76
|
+
warnings.push("Very high event density (>20/sec) - consider adjusting threshold");
|
|
77
|
+
} else if (durationSec > 10 && eventsPerSecond < 0.1) {
|
|
78
|
+
warnings.push("Very sparse events (<0.1/sec) - signal may not be active");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
eventCount,
|
|
83
|
+
eventsPerSecond,
|
|
84
|
+
warnings,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ----------------------------
|
|
89
|
+
// Band Event Functions
|
|
90
|
+
// ----------------------------
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Extract onset peaks from a band MIR 1D signal.
|
|
94
|
+
*
|
|
95
|
+
* Uses peak picking with conservative defaults optimized for band-scoped
|
|
96
|
+
* extraction. Typically applied to bandOnsetStrength or bandAmplitudeEnvelope.
|
|
97
|
+
*
|
|
98
|
+
* @param signal - The band MIR 1D result to extract peaks from
|
|
99
|
+
* @param options - Peak picking options
|
|
100
|
+
* @returns Band events result with onset peaks
|
|
101
|
+
*/
|
|
102
|
+
export function bandOnsetPeaks(
|
|
103
|
+
signal: BandMir1DResult,
|
|
104
|
+
options?: BandOnsetPeaksOptions
|
|
105
|
+
): BandEventsResult {
|
|
106
|
+
const startMs = nowMs();
|
|
107
|
+
|
|
108
|
+
const minIntervalSec = options?.minIntervalSec ?? 0.0625;
|
|
109
|
+
const adaptiveFactor = options?.adaptiveFactor ?? 0.8;
|
|
110
|
+
const strict = options?.strict ?? true;
|
|
111
|
+
|
|
112
|
+
const { times, values } = signal;
|
|
113
|
+
|
|
114
|
+
// Peak pick the signal
|
|
115
|
+
const pickOptions: PeakPickOptions = {
|
|
116
|
+
minIntervalSec,
|
|
117
|
+
adaptive: {
|
|
118
|
+
method: "meanStd",
|
|
119
|
+
factor: adaptiveFactor,
|
|
120
|
+
},
|
|
121
|
+
strict,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const peaks = peakPick(times, values, pickOptions);
|
|
125
|
+
|
|
126
|
+
// Normalize weights to 0-1 range
|
|
127
|
+
const maxStrength = peaks.reduce((max, p) => Math.max(max, p.strength), 0);
|
|
128
|
+
|
|
129
|
+
const events: BandMirEvent[] = peaks.map((p) => ({
|
|
130
|
+
time: p.time,
|
|
131
|
+
weight: maxStrength > 0 ? p.strength / maxStrength : 1,
|
|
132
|
+
}));
|
|
133
|
+
|
|
134
|
+
// Compute duration for diagnostics
|
|
135
|
+
const duration = times.length >= 2
|
|
136
|
+
? (times[times.length - 1] ?? 0) - (times[0] ?? 0)
|
|
137
|
+
: 0;
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
kind: "bandEvents",
|
|
141
|
+
bandId: signal.bandId,
|
|
142
|
+
bandLabel: signal.bandLabel,
|
|
143
|
+
fn: "bandOnsetPeaks",
|
|
144
|
+
events,
|
|
145
|
+
sourceSignal: {
|
|
146
|
+
fn: signal.fn,
|
|
147
|
+
times: signal.times,
|
|
148
|
+
values: signal.values,
|
|
149
|
+
},
|
|
150
|
+
meta: createMeta(startMs),
|
|
151
|
+
diagnostics: computeEventDiagnostics(events, duration),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Extract beat candidates from a band's onset peaks.
|
|
157
|
+
*
|
|
158
|
+
* Similar to full-track beat candidate detection but simplified for
|
|
159
|
+
* single-band input. Uses the onset peaks to identify beat-like events.
|
|
160
|
+
*
|
|
161
|
+
* @param onsetPeaks - Band onset peaks result
|
|
162
|
+
* @param options - Beat candidate options
|
|
163
|
+
* @returns Band events result with beat candidates
|
|
164
|
+
*/
|
|
165
|
+
export function bandBeatCandidates(
|
|
166
|
+
onsetPeaks: BandEventsResult,
|
|
167
|
+
options?: BandBeatCandidatesOptions
|
|
168
|
+
): BandEventsResult {
|
|
169
|
+
const startMs = nowMs();
|
|
170
|
+
|
|
171
|
+
const minIntervalSec = options?.minIntervalSec ?? 0.1;
|
|
172
|
+
const thresholdFactor = options?.thresholdFactor ?? 0.5;
|
|
173
|
+
|
|
174
|
+
// Filter onset peaks by strength threshold
|
|
175
|
+
// Use adaptive threshold similar to full-track beat candidates
|
|
176
|
+
const weights = onsetPeaks.events.map((e) => e.weight);
|
|
177
|
+
const meanWeight = weights.length > 0
|
|
178
|
+
? weights.reduce((sum, w) => sum + w, 0) / weights.length
|
|
179
|
+
: 0;
|
|
180
|
+
|
|
181
|
+
// Simple std calculation
|
|
182
|
+
const variance = weights.length > 0
|
|
183
|
+
? weights.reduce((sum, w) => sum + (w - meanWeight) ** 2, 0) / weights.length
|
|
184
|
+
: 0;
|
|
185
|
+
const stdWeight = Math.sqrt(variance);
|
|
186
|
+
|
|
187
|
+
const threshold = meanWeight + thresholdFactor * stdWeight;
|
|
188
|
+
|
|
189
|
+
// Filter by threshold and minimum interval
|
|
190
|
+
const candidates: BandMirEvent[] = [];
|
|
191
|
+
let lastTime = -Infinity;
|
|
192
|
+
|
|
193
|
+
for (const event of onsetPeaks.events) {
|
|
194
|
+
if (event.weight < threshold) continue;
|
|
195
|
+
if (event.time - lastTime < minIntervalSec) {
|
|
196
|
+
// Keep stronger event within interval
|
|
197
|
+
const last = candidates[candidates.length - 1];
|
|
198
|
+
if (last && event.weight > last.weight) {
|
|
199
|
+
last.time = event.time;
|
|
200
|
+
last.weight = event.weight;
|
|
201
|
+
lastTime = event.time;
|
|
202
|
+
}
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
candidates.push({
|
|
207
|
+
time: event.time,
|
|
208
|
+
weight: event.weight,
|
|
209
|
+
beatPosition: event.beatPosition,
|
|
210
|
+
beatPhase: event.beatPhase,
|
|
211
|
+
});
|
|
212
|
+
lastTime = event.time;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Compute duration from source signal if available
|
|
216
|
+
const duration = onsetPeaks.sourceSignal?.times
|
|
217
|
+
? (onsetPeaks.sourceSignal.times.length >= 2
|
|
218
|
+
? (onsetPeaks.sourceSignal.times[onsetPeaks.sourceSignal.times.length - 1] ?? 0)
|
|
219
|
+
- (onsetPeaks.sourceSignal.times[0] ?? 0)
|
|
220
|
+
: 0)
|
|
221
|
+
: 0;
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
kind: "bandEvents",
|
|
225
|
+
bandId: onsetPeaks.bandId,
|
|
226
|
+
bandLabel: onsetPeaks.bandLabel,
|
|
227
|
+
fn: "bandBeatCandidates",
|
|
228
|
+
events: candidates,
|
|
229
|
+
// Don't include source signal to save memory
|
|
230
|
+
meta: createMeta(startMs),
|
|
231
|
+
diagnostics: computeEventDiagnostics(candidates, duration),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ----------------------------
|
|
236
|
+
// Batch Runner
|
|
237
|
+
// ----------------------------
|
|
238
|
+
|
|
239
|
+
export type BandEventsBatchRequest = {
|
|
240
|
+
/** Band MIR results to extract events from (keyed by bandId) */
|
|
241
|
+
bandMirResults: Map<string, BandMir1DResult[]>;
|
|
242
|
+
/** Event functions to run */
|
|
243
|
+
functions: BandEventFunctionId[];
|
|
244
|
+
/** Source signal function to use for onset peaks. Default: bandOnsetStrength */
|
|
245
|
+
sourceFunction?: "bandOnsetStrength" | "bandAmplitudeEnvelope";
|
|
246
|
+
/** Options for onset peaks extraction */
|
|
247
|
+
onsetPeaksOptions?: BandOnsetPeaksOptions;
|
|
248
|
+
/** Options for beat candidates extraction */
|
|
249
|
+
beatCandidatesOptions?: BandBeatCandidatesOptions;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
export type BandEventsBatchResult = {
|
|
253
|
+
/** Results keyed by bandId, each containing results for requested functions */
|
|
254
|
+
results: Map<string, BandEventsResult[]>;
|
|
255
|
+
/** Total computation time in ms */
|
|
256
|
+
totalTimingMs: number;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Run band event extraction for multiple bands.
|
|
261
|
+
*
|
|
262
|
+
* @param request - Batch request specifying bands, functions, and options
|
|
263
|
+
* @returns Map of results by band ID
|
|
264
|
+
*/
|
|
265
|
+
export async function runBandEventsBatch(
|
|
266
|
+
request: BandEventsBatchRequest
|
|
267
|
+
): Promise<BandEventsBatchResult> {
|
|
268
|
+
const startMs = nowMs();
|
|
269
|
+
|
|
270
|
+
const results = new Map<string, BandEventsResult[]>();
|
|
271
|
+
const sourceFunction = request.sourceFunction ?? "bandOnsetStrength";
|
|
272
|
+
|
|
273
|
+
for (const [bandId, mirResults] of request.bandMirResults.entries()) {
|
|
274
|
+
const bandEventResults: BandEventsResult[] = [];
|
|
275
|
+
|
|
276
|
+
// Find the source signal for onset peaks
|
|
277
|
+
const sourceSignal = mirResults.find((r) => r.fn === sourceFunction);
|
|
278
|
+
if (!sourceSignal) {
|
|
279
|
+
// Skip if source signal not available
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
for (const fn of request.functions) {
|
|
284
|
+
switch (fn) {
|
|
285
|
+
case "bandOnsetPeaks": {
|
|
286
|
+
const result = bandOnsetPeaks(sourceSignal, request.onsetPeaksOptions);
|
|
287
|
+
bandEventResults.push(result);
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
case "bandBeatCandidates": {
|
|
291
|
+
// Beat candidates require onset peaks first
|
|
292
|
+
let onsetPeaksResult = bandEventResults.find(
|
|
293
|
+
(r) => r.fn === "bandOnsetPeaks"
|
|
294
|
+
);
|
|
295
|
+
if (!onsetPeaksResult) {
|
|
296
|
+
// Compute onset peaks if not already done
|
|
297
|
+
onsetPeaksResult = bandOnsetPeaks(
|
|
298
|
+
sourceSignal,
|
|
299
|
+
request.onsetPeaksOptions
|
|
300
|
+
);
|
|
301
|
+
// Only add to results if explicitly requested
|
|
302
|
+
if (!request.functions.includes("bandOnsetPeaks")) {
|
|
303
|
+
// Don't add to results, just use for beat candidates
|
|
304
|
+
} else {
|
|
305
|
+
bandEventResults.push(onsetPeaksResult);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const result = bandBeatCandidates(
|
|
309
|
+
onsetPeaksResult,
|
|
310
|
+
request.beatCandidatesOptions
|
|
311
|
+
);
|
|
312
|
+
bandEventResults.push(result);
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
default: {
|
|
316
|
+
// Exhaustive check
|
|
317
|
+
const _exhaustive: never = fn;
|
|
318
|
+
throw new Error(`Unknown band event function: ${_exhaustive}`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (bandEventResults.length > 0) {
|
|
324
|
+
results.set(bandId, bandEventResults);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const endMs = nowMs();
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
results,
|
|
332
|
+
totalTimingMs: endMs - startMs,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Get a human-readable label for a band event function.
|
|
338
|
+
*
|
|
339
|
+
* @param fn - Band event function ID
|
|
340
|
+
* @returns Human-readable label
|
|
341
|
+
*/
|
|
342
|
+
export function getBandEventFunctionLabel(fn: BandEventFunctionId): string {
|
|
343
|
+
switch (fn) {
|
|
344
|
+
case "bandOnsetPeaks":
|
|
345
|
+
return "Onset Peaks";
|
|
346
|
+
case "bandBeatCandidates":
|
|
347
|
+
return "Beat Candidates";
|
|
348
|
+
default:
|
|
349
|
+
return fn;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Band Masking utilities for F3.
|
|
3
|
+
*
|
|
4
|
+
* These functions apply frequency band boundaries as spectral masks
|
|
5
|
+
* to an existing spectrogram, enabling per-band MIR analysis.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Spectrogram } from "./spectrogram";
|
|
9
|
+
import type { FrequencyBand } from "../types";
|
|
10
|
+
import { frequencyBoundsAt } from "./frequencyBand";
|
|
11
|
+
|
|
12
|
+
// ----------------------------
|
|
13
|
+
// Types
|
|
14
|
+
// ----------------------------
|
|
15
|
+
|
|
16
|
+
export type BandMaskOptions = {
|
|
17
|
+
/** Soft edge width in Hz for smooth transitions (0 = hard edge). Default: 0 */
|
|
18
|
+
edgeSmoothHz?: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type MaskedSpectrogram = Spectrogram & {
|
|
22
|
+
/** ID of the band this mask was computed for */
|
|
23
|
+
bandId: string;
|
|
24
|
+
/** Fraction of energy retained per frame (0-1) for diagnostics */
|
|
25
|
+
energyRetainedPerFrame: Float32Array;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// ----------------------------
|
|
29
|
+
// Conversion Helpers
|
|
30
|
+
// ----------------------------
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Convert an FFT bin index to frequency in Hz.
|
|
34
|
+
*
|
|
35
|
+
* @param bin - FFT bin index (0 to fftSize/2)
|
|
36
|
+
* @param sampleRate - Audio sample rate in Hz
|
|
37
|
+
* @param fftSize - FFT size (number of samples)
|
|
38
|
+
* @returns Frequency in Hz
|
|
39
|
+
*/
|
|
40
|
+
export function binToHz(bin: number, sampleRate: number, fftSize: number): number {
|
|
41
|
+
return bin * (sampleRate / fftSize);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Convert a frequency in Hz to an FFT bin index.
|
|
46
|
+
*
|
|
47
|
+
* @param hz - Frequency in Hz
|
|
48
|
+
* @param sampleRate - Audio sample rate in Hz
|
|
49
|
+
* @param fftSize - FFT size (number of samples)
|
|
50
|
+
* @returns FFT bin index (may be fractional)
|
|
51
|
+
*/
|
|
52
|
+
export function hzToBin(hz: number, sampleRate: number, fftSize: number): number {
|
|
53
|
+
return hz / (sampleRate / fftSize);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ----------------------------
|
|
57
|
+
// Mask Computation
|
|
58
|
+
// ----------------------------
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Compute a spectral mask for a band at a specific time.
|
|
62
|
+
*
|
|
63
|
+
* Returns a Float32Array of length (fftSize/2 + 1) containing
|
|
64
|
+
* mask values between 0 and 1.
|
|
65
|
+
*
|
|
66
|
+
* @param band - The frequency band to mask
|
|
67
|
+
* @param time - Time in seconds
|
|
68
|
+
* @param sampleRate - Audio sample rate in Hz
|
|
69
|
+
* @param fftSize - FFT size
|
|
70
|
+
* @param options - Mask options
|
|
71
|
+
* @returns Mask array, or null if band is inactive at this time
|
|
72
|
+
*/
|
|
73
|
+
export function computeBandMaskAtTime(
|
|
74
|
+
band: FrequencyBand,
|
|
75
|
+
time: number,
|
|
76
|
+
sampleRate: number,
|
|
77
|
+
fftSize: number,
|
|
78
|
+
options?: BandMaskOptions
|
|
79
|
+
): Float32Array | null {
|
|
80
|
+
const bounds = frequencyBoundsAt(band, time);
|
|
81
|
+
if (!bounds) return null;
|
|
82
|
+
|
|
83
|
+
const nBins = (fftSize >>> 1) + 1;
|
|
84
|
+
const mask = new Float32Array(nBins);
|
|
85
|
+
const edgeSmoothHz = options?.edgeSmoothHz ?? 0;
|
|
86
|
+
const binHz = sampleRate / fftSize;
|
|
87
|
+
|
|
88
|
+
for (let k = 0; k < nBins; k++) {
|
|
89
|
+
const hz = binToHz(k, sampleRate, fftSize);
|
|
90
|
+
|
|
91
|
+
if (hz < bounds.lowHz || hz > bounds.highHz) {
|
|
92
|
+
// Outside band
|
|
93
|
+
mask[k] = 0;
|
|
94
|
+
} else if (edgeSmoothHz <= 0) {
|
|
95
|
+
// Inside band, hard edge
|
|
96
|
+
mask[k] = 1;
|
|
97
|
+
} else {
|
|
98
|
+
// Inside band, with soft edges
|
|
99
|
+
const distFromLow = hz - bounds.lowHz;
|
|
100
|
+
const distFromHigh = bounds.highHz - hz;
|
|
101
|
+
|
|
102
|
+
let gain = 1;
|
|
103
|
+
|
|
104
|
+
// Apply raised-cosine taper at low edge
|
|
105
|
+
if (distFromLow < edgeSmoothHz) {
|
|
106
|
+
gain *= 0.5 * (1 - Math.cos(Math.PI * distFromLow / edgeSmoothHz));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Apply raised-cosine taper at high edge
|
|
110
|
+
if (distFromHigh < edgeSmoothHz) {
|
|
111
|
+
gain *= 0.5 * (1 - Math.cos(Math.PI * distFromHigh / edgeSmoothHz));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
mask[k] = gain;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return mask;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Apply a band mask to an entire spectrogram.
|
|
123
|
+
*
|
|
124
|
+
* Creates a new spectrogram with band-masked magnitudes.
|
|
125
|
+
* Uses linear interpolation of band bounds for time-varying bands.
|
|
126
|
+
*
|
|
127
|
+
* @param spec - Source spectrogram
|
|
128
|
+
* @param band - Frequency band to apply
|
|
129
|
+
* @param options - Mask options
|
|
130
|
+
* @returns Masked spectrogram with energy retention diagnostics
|
|
131
|
+
*/
|
|
132
|
+
export function applyBandMaskToSpectrogram(
|
|
133
|
+
spec: Spectrogram,
|
|
134
|
+
band: FrequencyBand,
|
|
135
|
+
options?: BandMaskOptions
|
|
136
|
+
): MaskedSpectrogram {
|
|
137
|
+
const nFrames = spec.times.length;
|
|
138
|
+
const nBins = (spec.fftSize >>> 1) + 1;
|
|
139
|
+
|
|
140
|
+
const maskedMagnitudes: Float32Array[] = new Array(nFrames);
|
|
141
|
+
const energyRetained = new Float32Array(nFrames);
|
|
142
|
+
|
|
143
|
+
for (let t = 0; t < nFrames; t++) {
|
|
144
|
+
const time = spec.times[t] ?? 0;
|
|
145
|
+
const srcMags = spec.magnitudes[t];
|
|
146
|
+
|
|
147
|
+
if (!srcMags) {
|
|
148
|
+
maskedMagnitudes[t] = new Float32Array(nBins);
|
|
149
|
+
energyRetained[t] = 0;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Compute mask for this frame
|
|
154
|
+
const mask = computeBandMaskAtTime(
|
|
155
|
+
band,
|
|
156
|
+
time,
|
|
157
|
+
spec.sampleRate,
|
|
158
|
+
spec.fftSize,
|
|
159
|
+
options
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
if (!mask) {
|
|
163
|
+
// Band is inactive at this time
|
|
164
|
+
maskedMagnitudes[t] = new Float32Array(nBins);
|
|
165
|
+
energyRetained[t] = 0;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Apply mask and compute energy
|
|
170
|
+
const masked = new Float32Array(nBins);
|
|
171
|
+
let originalEnergy = 0;
|
|
172
|
+
let maskedEnergy = 0;
|
|
173
|
+
|
|
174
|
+
for (let k = 0; k < nBins; k++) {
|
|
175
|
+
const mag = srcMags[k] ?? 0;
|
|
176
|
+
const maskedMag = mag * (mask[k] ?? 0);
|
|
177
|
+
|
|
178
|
+
masked[k] = maskedMag;
|
|
179
|
+
originalEnergy += mag * mag;
|
|
180
|
+
maskedEnergy += maskedMag * maskedMag;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
maskedMagnitudes[t] = masked;
|
|
184
|
+
energyRetained[t] = originalEnergy > 0 ? maskedEnergy / originalEnergy : 0;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
sampleRate: spec.sampleRate,
|
|
189
|
+
fftSize: spec.fftSize,
|
|
190
|
+
hopSize: spec.hopSize,
|
|
191
|
+
times: spec.times,
|
|
192
|
+
magnitudes: maskedMagnitudes,
|
|
193
|
+
bandId: band.id,
|
|
194
|
+
energyRetainedPerFrame: energyRetained,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Compute the energy of a magnitude spectrum.
|
|
200
|
+
*
|
|
201
|
+
* @param magnitudes - Magnitude spectrum (one frame)
|
|
202
|
+
* @returns Sum of squared magnitudes
|
|
203
|
+
*/
|
|
204
|
+
export function computeFrameEnergy(magnitudes: Float32Array): number {
|
|
205
|
+
let energy = 0;
|
|
206
|
+
for (let k = 0; k < magnitudes.length; k++) {
|
|
207
|
+
const mag = magnitudes[k] ?? 0;
|
|
208
|
+
energy += mag * mag;
|
|
209
|
+
}
|
|
210
|
+
return energy;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Compute the amplitude (sum of magnitudes) of a frame.
|
|
215
|
+
*
|
|
216
|
+
* @param magnitudes - Magnitude spectrum (one frame)
|
|
217
|
+
* @returns Sum of magnitudes
|
|
218
|
+
*/
|
|
219
|
+
export function computeFrameAmplitude(magnitudes: Float32Array): number {
|
|
220
|
+
let sum = 0;
|
|
221
|
+
for (let k = 0; k < magnitudes.length; k++) {
|
|
222
|
+
sum += magnitudes[k] ?? 0;
|
|
223
|
+
}
|
|
224
|
+
return sum;
|
|
225
|
+
}
|