@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,462 @@
1
+ /**
2
+ * CQT-Derived 1D Signals for F5.
3
+ *
4
+ * These signals extract musically meaningful 1D features from the CQT representation:
5
+ * - harmonicEnergy: Measures tonal presence vs noise
6
+ * - bassPitchMotion: Measures bassline activity and low-end groove
7
+ * - tonalStability: Measures harmonic stability vs modulation
8
+ *
9
+ * All signals are:
10
+ * - 1D (one value per frame)
11
+ * - Time-aligned with the CQT frames
12
+ * - Deterministic
13
+ * - Normalized to [0, 1] range
14
+ */
15
+
16
+ import type { CqtSignalId, CqtSignalResult, CqtSpectrogram, MirRunMeta } from "../types";
17
+ import { cqtBinToHz, hzToCqtBin } from "./cqt";
18
+
19
+ // ----------------------------
20
+ // Configuration Constants
21
+ // ----------------------------
22
+
23
+ /** Bass frequency range for bassPitchMotion */
24
+ const BASS_MIN_HZ = 20;
25
+ const BASS_MAX_HZ = 300;
26
+
27
+ /** Window size for tonal stability analysis (in frames) */
28
+ const TONAL_STABILITY_WINDOW_FRAMES = 20; // ~500ms at typical hop sizes
29
+
30
+ /** Number of chroma bins (one per semitone) */
31
+ const CHROMA_BINS = 12;
32
+
33
+ // ----------------------------
34
+ // Utility Functions
35
+ // ----------------------------
36
+
37
+ /**
38
+ * Normalize an array to [0, 1] range using min-max scaling.
39
+ */
40
+ function normalizeMinMax(values: Float32Array): Float32Array {
41
+ let min = Infinity;
42
+ let max = -Infinity;
43
+
44
+ for (let i = 0; i < values.length; i++) {
45
+ const v = values[i] ?? 0;
46
+ if (v < min) min = v;
47
+ if (v > max) max = v;
48
+ }
49
+
50
+ const range = max - min;
51
+ const result = new Float32Array(values.length);
52
+
53
+ if (range > 0) {
54
+ for (let i = 0; i < values.length; i++) {
55
+ result[i] = ((values[i] ?? 0) - min) / range;
56
+ }
57
+ } else {
58
+ // All values are the same - return 0.5
59
+ result.fill(0.5);
60
+ }
61
+
62
+ return result;
63
+ }
64
+
65
+ /**
66
+ * Compute the weighted centroid of an array.
67
+ */
68
+ function weightedCentroid(values: Float32Array, startIndex: number = 0): number {
69
+ let sumWeighted = 0;
70
+ let sumWeights = 0;
71
+
72
+ for (let i = 0; i < values.length; i++) {
73
+ const weight = values[i] ?? 0;
74
+ sumWeighted += (startIndex + i) * weight;
75
+ sumWeights += weight;
76
+ }
77
+
78
+ return sumWeights > 0 ? sumWeighted / sumWeights : startIndex + values.length / 2;
79
+ }
80
+
81
+ /**
82
+ * Compute variance of an array.
83
+ */
84
+ function variance(values: Float32Array): number {
85
+ if (values.length === 0) return 0;
86
+
87
+ let sum = 0;
88
+ for (let i = 0; i < values.length; i++) {
89
+ sum += values[i] ?? 0;
90
+ }
91
+ const mean = sum / values.length;
92
+
93
+ let sumSquaredDiff = 0;
94
+ for (let i = 0; i < values.length; i++) {
95
+ const diff = (values[i] ?? 0) - mean;
96
+ sumSquaredDiff += diff * diff;
97
+ }
98
+
99
+ return sumSquaredDiff / values.length;
100
+ }
101
+
102
+ // ----------------------------
103
+ // Harmonic Energy
104
+ // ----------------------------
105
+
106
+ /**
107
+ * Compute harmonic energy for a single CQT frame.
108
+ *
109
+ * Harmonic energy measures the ratio of energy concentrated at harmonic
110
+ * intervals (integer multiples of a fundamental) vs total energy.
111
+ *
112
+ * Algorithm:
113
+ * 1. Find the strongest bin as a candidate fundamental
114
+ * 2. Sum energy at harmonic positions (2x, 3x, 4x, ... of fundamental)
115
+ * 3. Divide by total energy
116
+ *
117
+ * High values indicate tonal/harmonic content; low values indicate noise.
118
+ */
119
+ function computeHarmonicEnergyFrame(
120
+ frame: Float32Array,
121
+ cqt: CqtSpectrogram
122
+ ): number {
123
+ if (frame.length === 0) return 0;
124
+
125
+ // Find total energy
126
+ let totalEnergy = 0;
127
+ for (let i = 0; i < frame.length; i++) {
128
+ const mag = frame[i] ?? 0;
129
+ totalEnergy += mag * mag;
130
+ }
131
+
132
+ if (totalEnergy === 0) return 0;
133
+
134
+ // Find the strongest bin as fundamental candidate
135
+ let maxMag = 0;
136
+ let fundamentalBin = 0;
137
+ for (let i = 0; i < frame.length; i++) {
138
+ const mag = frame[i] ?? 0;
139
+ if (mag > maxMag) {
140
+ maxMag = mag;
141
+ fundamentalBin = i;
142
+ }
143
+ }
144
+
145
+ const fundamentalFreq = cqtBinToHz(fundamentalBin, cqt.config);
146
+
147
+ // Sum energy at harmonic positions
148
+ let harmonicEnergy = 0;
149
+ const numHarmonics = 6; // Check first 6 harmonics
150
+
151
+ for (let h = 1; h <= numHarmonics; h++) {
152
+ const harmonicFreq = fundamentalFreq * h;
153
+ const harmonicBin = Math.round(hzToCqtBin(harmonicFreq, cqt.config));
154
+
155
+ if (harmonicBin >= 0 && harmonicBin < frame.length) {
156
+ const mag = frame[harmonicBin] ?? 0;
157
+ // Weight lower harmonics more heavily
158
+ const weight = 1 / h;
159
+ harmonicEnergy += mag * mag * weight;
160
+ }
161
+ }
162
+
163
+ // Normalize by expected harmonic weight sum
164
+ let weightSum = 0;
165
+ for (let h = 1; h <= numHarmonics; h++) {
166
+ weightSum += 1 / h;
167
+ }
168
+ harmonicEnergy /= weightSum;
169
+
170
+ // Return ratio of harmonic to total energy
171
+ return Math.min(1, harmonicEnergy / totalEnergy);
172
+ }
173
+
174
+ /**
175
+ * Compute harmonic energy signal from CQT.
176
+ *
177
+ * Measures sustained, pitch-aligned energy across harmonic bins.
178
+ * Intended to capture "tonal presence" vs noise.
179
+ */
180
+ export function harmonicEnergy(cqt: CqtSpectrogram): CqtSignalResult {
181
+ const startTime = performance.now();
182
+ const nFrames = cqt.magnitudes.length;
183
+ const values = new Float32Array(nFrames);
184
+
185
+ for (let frame = 0; frame < nFrames; frame++) {
186
+ const cqtFrame = cqt.magnitudes[frame];
187
+ if (cqtFrame) {
188
+ values[frame] = computeHarmonicEnergyFrame(cqtFrame, cqt);
189
+ }
190
+ }
191
+
192
+ // Normalize to [0, 1]
193
+ const normalized = normalizeMinMax(values);
194
+
195
+ const endTime = performance.now();
196
+
197
+ return {
198
+ kind: "cqt1d",
199
+ signalId: "harmonicEnergy",
200
+ times: cqt.times,
201
+ values: normalized,
202
+ meta: {
203
+ backend: "cpu",
204
+ usedGpu: false,
205
+ timings: {
206
+ totalMs: endTime - startTime,
207
+ cpuMs: endTime - startTime,
208
+ },
209
+ },
210
+ };
211
+ }
212
+
213
+ // ----------------------------
214
+ // Bass Pitch Motion
215
+ // ----------------------------
216
+
217
+ /**
218
+ * Compute bass pitch motion signal from CQT.
219
+ *
220
+ * Measures rate and magnitude of pitch movement in low-frequency CQT bins.
221
+ * Intended to capture bassline motion, groove, and low-end activity.
222
+ *
223
+ * Algorithm:
224
+ * 1. Extract bass-range bins from each frame
225
+ * 2. Compute pitch centroid in bass range
226
+ * 3. Compute absolute difference between consecutive frames
227
+ */
228
+ export function bassPitchMotion(cqt: CqtSpectrogram): CqtSignalResult {
229
+ const startTime = performance.now();
230
+ const nFrames = cqt.magnitudes.length;
231
+
232
+ // Find bass bin range
233
+ const bassStartBin = Math.max(0, Math.floor(hzToCqtBin(BASS_MIN_HZ, cqt.config)));
234
+ const bassEndBin = Math.min(
235
+ cqt.magnitudes[0]?.length ?? 0,
236
+ Math.ceil(hzToCqtBin(BASS_MAX_HZ, cqt.config))
237
+ );
238
+ const bassNumBins = bassEndBin - bassStartBin;
239
+
240
+ if (bassNumBins <= 0) {
241
+ // No bass bins available
242
+ return {
243
+ kind: "cqt1d",
244
+ signalId: "bassPitchMotion",
245
+ times: cqt.times,
246
+ values: new Float32Array(nFrames),
247
+ meta: {
248
+ backend: "cpu",
249
+ usedGpu: false,
250
+ timings: { totalMs: 0, cpuMs: 0 },
251
+ },
252
+ };
253
+ }
254
+
255
+ // Compute bass centroid for each frame
256
+ const centroids = new Float32Array(nFrames);
257
+
258
+ for (let frame = 0; frame < nFrames; frame++) {
259
+ const cqtFrame = cqt.magnitudes[frame];
260
+ if (!cqtFrame) continue;
261
+
262
+ // Extract bass bins
263
+ const bassBins = new Float32Array(bassNumBins);
264
+ for (let i = 0; i < bassNumBins; i++) {
265
+ bassBins[i] = cqtFrame[bassStartBin + i] ?? 0;
266
+ }
267
+
268
+ // Compute weighted centroid
269
+ centroids[frame] = weightedCentroid(bassBins, bassStartBin);
270
+ }
271
+
272
+ // Compute motion as absolute difference between consecutive frames
273
+ const motion = new Float32Array(nFrames);
274
+ for (let frame = 1; frame < nFrames; frame++) {
275
+ motion[frame] = Math.abs((centroids[frame] ?? 0) - (centroids[frame - 1] ?? 0));
276
+ }
277
+ motion[0] = motion[1] ?? 0; // First frame has no previous
278
+
279
+ // Normalize to [0, 1]
280
+ const normalized = normalizeMinMax(motion);
281
+
282
+ const endTime = performance.now();
283
+
284
+ return {
285
+ kind: "cqt1d",
286
+ signalId: "bassPitchMotion",
287
+ times: cqt.times,
288
+ values: normalized,
289
+ meta: {
290
+ backend: "cpu",
291
+ usedGpu: false,
292
+ timings: {
293
+ totalMs: endTime - startTime,
294
+ cpuMs: endTime - startTime,
295
+ },
296
+ },
297
+ };
298
+ }
299
+
300
+ // ----------------------------
301
+ // Tonal Stability
302
+ // ----------------------------
303
+
304
+ /**
305
+ * Fold CQT bins into chroma (12 semitones).
306
+ */
307
+ function computeChroma(frame: Float32Array, binsPerOctave: number): Float32Array {
308
+ const chroma = new Float32Array(CHROMA_BINS);
309
+ const binsPerSemitone = binsPerOctave / CHROMA_BINS;
310
+
311
+ for (let i = 0; i < frame.length; i++) {
312
+ // Map CQT bin to chroma bin
313
+ const chromaBin = Math.floor((i % binsPerOctave) / binsPerSemitone) % CHROMA_BINS;
314
+ const mag = frame[i] ?? 0;
315
+ chroma[chromaBin] = (chroma[chromaBin] ?? 0) + mag * mag; // Energy
316
+ }
317
+
318
+ // Normalize
319
+ let sum = 0;
320
+ for (let i = 0; i < CHROMA_BINS; i++) {
321
+ sum += chroma[i] ?? 0;
322
+ }
323
+ if (sum > 0) {
324
+ for (let i = 0; i < CHROMA_BINS; i++) {
325
+ chroma[i] = (chroma[i] ?? 0) / sum;
326
+ }
327
+ }
328
+
329
+ return chroma;
330
+ }
331
+
332
+ /**
333
+ * Compute tonal stability signal from CQT.
334
+ *
335
+ * Measures consistency of dominant pitch structure over time.
336
+ * High values imply harmonic stability; low values imply modulation or noise.
337
+ *
338
+ * Algorithm:
339
+ * 1. Compute chroma histogram for each frame
340
+ * 2. Over a sliding window, compute variance of chroma distribution
341
+ * 3. Low variance = stable tonality; high variance = unstable
342
+ * 4. Invert so that high values = stable
343
+ */
344
+ export function tonalStability(cqt: CqtSpectrogram): CqtSignalResult {
345
+ const startTime = performance.now();
346
+ const nFrames = cqt.magnitudes.length;
347
+
348
+ // Compute chroma for each frame
349
+ const chromas: Float32Array[] = new Array(nFrames);
350
+ for (let frame = 0; frame < nFrames; frame++) {
351
+ const cqtFrame = cqt.magnitudes[frame];
352
+ if (cqtFrame) {
353
+ chromas[frame] = computeChroma(cqtFrame, cqt.binsPerOctave);
354
+ } else {
355
+ chromas[frame] = new Float32Array(CHROMA_BINS);
356
+ }
357
+ }
358
+
359
+ // Compute stability over sliding window
360
+ const halfWindow = Math.floor(TONAL_STABILITY_WINDOW_FRAMES / 2);
361
+ const instability = new Float32Array(nFrames);
362
+
363
+ for (let frame = 0; frame < nFrames; frame++) {
364
+ // Define window bounds
365
+ const windowStart = Math.max(0, frame - halfWindow);
366
+ const windowEnd = Math.min(nFrames, frame + halfWindow + 1);
367
+ const windowSize = windowEnd - windowStart;
368
+
369
+ // Compute average chroma over window
370
+ const avgChroma = new Float32Array(CHROMA_BINS);
371
+ for (let w = windowStart; w < windowEnd; w++) {
372
+ const chroma = chromas[w];
373
+ if (chroma) {
374
+ for (let c = 0; c < CHROMA_BINS; c++) {
375
+ avgChroma[c] = (avgChroma[c] ?? 0) + (chroma[c] ?? 0);
376
+ }
377
+ }
378
+ }
379
+ for (let c = 0; c < CHROMA_BINS; c++) {
380
+ avgChroma[c] = (avgChroma[c] ?? 0) / windowSize;
381
+ }
382
+
383
+ // Compute variance of chroma values within window
384
+ let totalVariance = 0;
385
+ for (let w = windowStart; w < windowEnd; w++) {
386
+ const chroma = chromas[w];
387
+ if (chroma) {
388
+ for (let c = 0; c < CHROMA_BINS; c++) {
389
+ const diff = (chroma[c] ?? 0) - (avgChroma[c] ?? 0);
390
+ totalVariance += diff * diff;
391
+ }
392
+ }
393
+ }
394
+ totalVariance /= windowSize * CHROMA_BINS;
395
+
396
+ instability[frame] = totalVariance;
397
+ }
398
+
399
+ // Normalize instability to [0, 1]
400
+ const normalizedInstability = normalizeMinMax(instability);
401
+
402
+ // Invert to get stability (high stability = low variance)
403
+ const stability = new Float32Array(nFrames);
404
+ for (let frame = 0; frame < nFrames; frame++) {
405
+ stability[frame] = 1 - (normalizedInstability[frame] ?? 0);
406
+ }
407
+
408
+ const endTime = performance.now();
409
+
410
+ return {
411
+ kind: "cqt1d",
412
+ signalId: "tonalStability",
413
+ times: cqt.times,
414
+ values: stability,
415
+ meta: {
416
+ backend: "cpu",
417
+ usedGpu: false,
418
+ timings: {
419
+ totalMs: endTime - startTime,
420
+ cpuMs: endTime - startTime,
421
+ },
422
+ },
423
+ };
424
+ }
425
+
426
+ // ----------------------------
427
+ // Unified Signal Computation
428
+ // ----------------------------
429
+
430
+ /**
431
+ * Compute a CQT-derived signal by ID.
432
+ */
433
+ export function computeCqtSignal(
434
+ cqt: CqtSpectrogram,
435
+ signalId: CqtSignalId
436
+ ): CqtSignalResult {
437
+ switch (signalId) {
438
+ case "harmonicEnergy":
439
+ return harmonicEnergy(cqt);
440
+ case "bassPitchMotion":
441
+ return bassPitchMotion(cqt);
442
+ case "tonalStability":
443
+ return tonalStability(cqt);
444
+ default:
445
+ throw new Error(`@octoseq/mir: unknown CQT signal ID: ${signalId}`);
446
+ }
447
+ }
448
+
449
+ /**
450
+ * Compute all CQT-derived signals.
451
+ */
452
+ export function computeAllCqtSignals(
453
+ cqt: CqtSpectrogram
454
+ ): Map<CqtSignalId, CqtSignalResult> {
455
+ const results = new Map<CqtSignalId, CqtSignalResult>();
456
+
457
+ results.set("harmonicEnergy", harmonicEnergy(cqt));
458
+ results.set("bassPitchMotion", bassPitchMotion(cqt));
459
+ results.set("tonalStability", tonalStability(cqt));
460
+
461
+ return results;
462
+ }