@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.
Files changed (47) hide show
  1. package/dist/chunk-HF3QHCRK.js +3234 -0
  2. package/dist/chunk-HF3QHCRK.js.map +1 -0
  3. package/dist/index.d.ts +2122 -47
  4. package/dist/index.js +3524 -39
  5. package/dist/index.js.map +1 -1
  6. package/dist/runMir-CnJQbBr8.d.ts +187 -0
  7. package/dist/runner/runMir.d.ts +2 -2
  8. package/dist/runner/runMir.js +1 -1
  9. package/dist/runner/workerProtocol.d.ts +8 -1
  10. package/dist/runner/workerProtocol.js.map +1 -1
  11. package/dist/types-DqH4umN8.d.ts +761 -0
  12. package/package.json +2 -2
  13. package/src/dsp/activity.ts +544 -0
  14. package/src/dsp/bandCqt.ts +662 -0
  15. package/src/dsp/bandEvents.ts +351 -0
  16. package/src/dsp/bandMask.ts +225 -0
  17. package/src/dsp/bandMir.ts +524 -0
  18. package/src/dsp/bandProposal.ts +552 -0
  19. package/src/dsp/beatCandidates.ts +299 -0
  20. package/src/dsp/cqt.ts +386 -0
  21. package/src/dsp/cqtSignals.ts +462 -0
  22. package/src/dsp/customSignalReduction.ts +841 -0
  23. package/src/dsp/eventToSignal.ts +531 -0
  24. package/src/dsp/frequencyBand.ts +956 -0
  25. package/src/dsp/mel.ts +56 -3
  26. package/src/dsp/musicalTime.ts +240 -0
  27. package/src/dsp/onset.ts +296 -30
  28. package/src/dsp/peakPicking.ts +519 -0
  29. package/src/dsp/phaseAlignment.ts +153 -0
  30. package/src/dsp/pitch.ts +289 -0
  31. package/src/dsp/resample.ts +44 -0
  32. package/src/dsp/signalTransforms.ts +660 -0
  33. package/src/dsp/silenceGating.ts +511 -0
  34. package/src/dsp/spectral.ts +124 -4
  35. package/src/dsp/tempoHypotheses.ts +395 -0
  36. package/src/gpu/bufferPool.ts +266 -0
  37. package/src/gpu/context.ts +30 -6
  38. package/src/gpu/helpers.ts +85 -0
  39. package/src/gpu/melProject.ts +83 -0
  40. package/src/index.ts +366 -5
  41. package/src/runner/runMir.ts +234 -2
  42. package/src/runner/workerProtocol.ts +9 -1
  43. package/src/types.ts +768 -3
  44. package/dist/chunk-DUWYCAVG.js +0 -1525
  45. package/dist/chunk-DUWYCAVG.js.map +0 -1
  46. package/dist/runMir-CSIBwNZ3.d.ts +0 -84
  47. 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
+ }