@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
package/dist/index.js CHANGED
@@ -1,36 +1,3499 @@
1
- import { peakPick } from './chunk-DUWYCAVG.js';
2
- export { delta, deltaDelta, hpss, melSpectrogram, mfcc, onsetEnvelopeFromMel, onsetEnvelopeFromMelGpu, onsetEnvelopeFromSpectrogram, peakPick, runMir, spectralCentroid, spectralFlux, spectrogram } from './chunk-DUWYCAVG.js';
1
+ import { peakPick, hzToCqtBin, cqtBinToHz, withCqtDefaults, cqtSpectrogram, harmonicEnergy, bassPitchMotion, tonalStability, getNumBins } from './chunk-HF3QHCRK.js';
2
+ export { CQT_DEFAULTS, amplitudeEnvelope, applyActivityGating, bassPitchMotion, beatSalienceFromMel, buildActivityMask, computeActivityFromAudio, computeActivityFromMel, computeActivityFromSpectrogram, computeAllCqtSignals, computeCqt, computeCqtSignal, computeFrameEnergyFromMel, computeFrameEnergyFromSpectrogram, computeSilenceGating, cqtBinToHz, cqtSpectrogram, delta, deltaDelta, detectBeatCandidates, estimateNoiseFloor, featureIndexToHz, generateTempoHypotheses, getCqtBinFrequencies, getNumBins, getNumOctaves, harmonicEnergy, hpss, hzToCqtBin, hzToFeatureIndex, hzToMel, interpolateActivity, melSpectrogram, melToHz, mfcc, onsetEnvelopeFromMel, onsetEnvelopeFromMelGpu, onsetEnvelopeFromSpectrogram, peakPick, pitchConfidence, pitchF0, runMir, spectralCentroid, spectralFlux, spectrogram, tonalStability, withBinGateDefaults, withCqtDefaults, withSilenceGateDefaults } from './chunk-HF3QHCRK.js';
3
+
4
+ // src/gpu/bufferPool.ts
5
+ var DEFAULT_OPTIONS = {
6
+ maxBuffersPerUsage: 32,
7
+ bufferTTLMs: 6e4,
8
+ enableAutoCleanup: true,
9
+ cleanupIntervalMs: 3e4
10
+ };
11
+ function makePoolKey(size, usage) {
12
+ return `${size}-${usage}`;
13
+ }
14
+ var BufferPool = class {
15
+ device;
16
+ options;
17
+ // Available buffers indexed by size+usage key
18
+ availableBuffers = /* @__PURE__ */ new Map();
19
+ // Currently in-use buffers (for tracking and stats)
20
+ activeBuffers = /* @__PURE__ */ new Set();
21
+ // Cleanup interval handle
22
+ cleanupInterval = null;
23
+ constructor(device, options = {}) {
24
+ this.device = device;
25
+ this.options = { ...DEFAULT_OPTIONS, ...options };
26
+ if (this.options.enableAutoCleanup) {
27
+ this.startAutoCleanup();
28
+ }
29
+ }
30
+ /**
31
+ * Acquire a buffer from the pool or create a new one if none available.
32
+ *
33
+ * @param size - Byte size of the buffer
34
+ * @param usage - Buffer usage flags (e.g. GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST)
35
+ * @returns A GPUBuffer ready for use
36
+ */
37
+ acquire(size, usage) {
38
+ const key = makePoolKey(size, usage);
39
+ const pool = this.availableBuffers.get(key);
40
+ if (pool && pool.length > 0) {
41
+ const entry = pool.pop();
42
+ this.activeBuffers.add(entry.buffer);
43
+ return entry.buffer;
44
+ }
45
+ const buffer = this.device.createBuffer({ size, usage });
46
+ this.activeBuffers.add(buffer);
47
+ return buffer;
48
+ }
49
+ /**
50
+ * Release a buffer back to the pool for reuse.
51
+ *
52
+ * @param buffer - The buffer to release
53
+ * @param size - Original size of the buffer
54
+ * @param usage - Original usage flags of the buffer
55
+ */
56
+ release(buffer, size, usage) {
57
+ if (!this.activeBuffers.has(buffer)) {
58
+ console.warn("@octoseq/mir: Attempting to release buffer not tracked by pool");
59
+ return;
60
+ }
61
+ this.activeBuffers.delete(buffer);
62
+ const key = makePoolKey(size, usage);
63
+ let pool = this.availableBuffers.get(key);
64
+ if (!pool) {
65
+ pool = [];
66
+ this.availableBuffers.set(key, pool);
67
+ }
68
+ if (pool.length >= this.options.maxBuffersPerUsage) {
69
+ buffer.destroy();
70
+ return;
71
+ }
72
+ pool.push({
73
+ buffer,
74
+ size,
75
+ usage,
76
+ lastUsed: performance.now()
77
+ });
78
+ }
79
+ /**
80
+ * Clean up old unused buffers that exceed the TTL.
81
+ */
82
+ cleanup() {
83
+ const now = performance.now();
84
+ const ttl = this.options.bufferTTLMs;
85
+ for (const [key, pool] of this.availableBuffers.entries()) {
86
+ const remaining = [];
87
+ for (const entry of pool) {
88
+ if (now - entry.lastUsed > ttl) {
89
+ entry.buffer.destroy();
90
+ } else {
91
+ remaining.push(entry);
92
+ }
93
+ }
94
+ if (remaining.length === 0) {
95
+ this.availableBuffers.delete(key);
96
+ } else {
97
+ this.availableBuffers.set(key, remaining);
98
+ }
99
+ }
100
+ }
101
+ /**
102
+ * Destroy all pooled buffers and clear the pool.
103
+ */
104
+ clear() {
105
+ for (const pool of this.availableBuffers.values()) {
106
+ for (const entry of pool) {
107
+ entry.buffer.destroy();
108
+ }
109
+ }
110
+ this.availableBuffers.clear();
111
+ }
112
+ /**
113
+ * Get statistics about the buffer pool.
114
+ */
115
+ getStats() {
116
+ let totalBuffers = this.activeBuffers.size;
117
+ let totalMemoryBytes = 0;
118
+ const poolsByUsage = /* @__PURE__ */ new Map();
119
+ for (const pool of this.availableBuffers.values()) {
120
+ totalBuffers += pool.length;
121
+ for (const entry of pool) {
122
+ totalMemoryBytes += entry.size;
123
+ const count = poolsByUsage.get(entry.usage) || 0;
124
+ poolsByUsage.set(entry.usage, count + 1);
125
+ }
126
+ }
127
+ return {
128
+ totalBuffers,
129
+ activeBuffers: this.activeBuffers.size,
130
+ availableBuffers: totalBuffers - this.activeBuffers.size,
131
+ poolsByUsage,
132
+ totalMemoryBytes
133
+ };
134
+ }
135
+ /**
136
+ * Start automatic cleanup of old buffers.
137
+ */
138
+ startAutoCleanup() {
139
+ if (this.cleanupInterval !== null) {
140
+ return;
141
+ }
142
+ this.cleanupInterval = setInterval(() => {
143
+ this.cleanup();
144
+ }, this.options.cleanupIntervalMs);
145
+ }
146
+ /**
147
+ * Stop automatic cleanup.
148
+ */
149
+ stopAutoCleanup() {
150
+ if (this.cleanupInterval !== null) {
151
+ clearInterval(this.cleanupInterval);
152
+ this.cleanupInterval = null;
153
+ }
154
+ }
155
+ /**
156
+ * Dispose of the buffer pool and destroy all buffers.
157
+ */
158
+ dispose() {
159
+ this.stopAutoCleanup();
160
+ this.clear();
161
+ }
162
+ };
3
163
 
4
164
  // src/gpu/context.ts
5
165
  var MirGPU = class _MirGPU {
6
166
  device;
7
167
  queue;
8
- constructor(device) {
168
+ bufferPool;
169
+ constructor(device, bufferPoolOptions) {
9
170
  this.device = device;
10
171
  this.queue = device.queue;
172
+ this.bufferPool = new BufferPool(device, bufferPoolOptions);
173
+ }
174
+ static async create(bufferPoolOptions) {
175
+ if (typeof navigator === "undefined") {
176
+ throw new Error(
177
+ "@octoseq/mir: WebGPU is only available in the browser (navigator is undefined)."
178
+ );
179
+ }
180
+ const nav = navigator;
181
+ if (!nav.gpu) {
182
+ throw new Error(
183
+ "@octoseq/mir: WebGPU is unavailable (navigator.gpu is missing). Use CPU mode or a WebGPU-capable browser."
184
+ );
185
+ }
186
+ const adapter = await nav.gpu.requestAdapter();
187
+ if (!adapter) {
188
+ throw new Error(
189
+ "@octoseq/mir: Failed to acquire a WebGPU adapter. WebGPU may be disabled or unsupported."
190
+ );
191
+ }
192
+ const device = await adapter.requestDevice();
193
+ return new _MirGPU(device, bufferPoolOptions);
194
+ }
195
+ /**
196
+ * Get statistics about buffer pool usage.
197
+ */
198
+ getBufferPoolStats() {
199
+ return this.bufferPool.getStats();
200
+ }
201
+ /**
202
+ * Manually trigger buffer pool cleanup (normally runs automatically).
203
+ */
204
+ cleanupBufferPool() {
205
+ this.bufferPool.cleanup();
206
+ }
207
+ /**
208
+ * Dispose of the GPU context and clean up all resources.
209
+ */
210
+ dispose() {
211
+ this.bufferPool.dispose();
212
+ }
213
+ };
214
+
215
+ // src/dsp/phaseAlignment.ts
216
+ var DEFAULT_CONFIG = {
217
+ phaseResolution: 16,
218
+ matchTolerance: 0.05,
219
+ // 50ms
220
+ topK: 3,
221
+ offsetPenaltyWeight: 0.2
222
+ };
223
+ function computePhaseHypotheses(bpm, candidates, audioDuration, config) {
224
+ const cfg = { ...DEFAULT_CONFIG, ...config };
225
+ const period = 60 / bpm;
226
+ if (candidates.length === 0 || audioDuration <= 0 || bpm <= 0) {
227
+ return [];
228
+ }
229
+ const phases = [];
230
+ for (let i = 0; i < cfg.phaseResolution; i++) {
231
+ const phaseOffset = i / cfg.phaseResolution * period;
232
+ const result = scorePhase(phaseOffset, period, candidates, audioDuration, cfg.matchTolerance);
233
+ phases.push({
234
+ index: i,
235
+ phaseOffset,
236
+ score: result.score,
237
+ matchCount: result.matchCount,
238
+ avgOffsetError: result.avgOffsetError
239
+ });
240
+ }
241
+ const maxScore = Math.max(...phases.map((p) => p.score), 1e-9);
242
+ for (const phase of phases) {
243
+ const penalty = phase.avgOffsetError / cfg.matchTolerance * cfg.offsetPenaltyWeight;
244
+ phase.score = phase.score / maxScore * (1 - Math.min(1, penalty));
245
+ }
246
+ phases.sort((a, b) => b.score - a.score);
247
+ return phases.slice(0, cfg.topK);
248
+ }
249
+ function scorePhase(phaseOffset, period, candidates, audioDuration, tolerance) {
250
+ let score = 0;
251
+ let matchCount = 0;
252
+ let totalOffsetError = 0;
253
+ for (const candidate of candidates) {
254
+ const beatsFromStart = (candidate.time - phaseOffset) / period;
255
+ const nearestBeatIndex = Math.round(beatsFromStart);
256
+ const nearestBeatTime = phaseOffset + nearestBeatIndex * period;
257
+ if (nearestBeatTime < 0 || nearestBeatTime > audioDuration) continue;
258
+ const offset = Math.abs(candidate.time - nearestBeatTime);
259
+ if (offset <= tolerance) {
260
+ const weight = Math.exp(-offset * offset / (2 * tolerance * tolerance));
261
+ score += candidate.strength * weight;
262
+ matchCount++;
263
+ totalOffsetError += offset;
264
+ }
265
+ }
266
+ const avgOffsetError = matchCount > 0 ? totalOffsetError / matchCount : 0;
267
+ return { score, matchCount, avgOffsetError };
268
+ }
269
+ function generateBeatTimes(bpm, phaseOffset, userNudge, audioDuration) {
270
+ if (bpm <= 0 || audioDuration <= 0) {
271
+ return [];
272
+ }
273
+ const period = 60 / bpm;
274
+ const effectivePhase = phaseOffset + userNudge;
275
+ const beats = [];
276
+ const firstBeatIndex = Math.ceil(-effectivePhase / period);
277
+ let time = effectivePhase + firstBeatIndex * period;
278
+ while (time <= audioDuration) {
279
+ if (time >= 0) {
280
+ beats.push(time);
281
+ }
282
+ time += period;
283
+ }
284
+ return beats;
285
+ }
286
+
287
+ // src/dsp/peakPicking.ts
288
+ var DEFAULT_PEAK_PICKING_PARAMS = {
289
+ threshold: 0.3,
290
+ minDistance: 0.1,
291
+ preMax: 2,
292
+ postMax: 2
293
+ };
294
+ function pickPeaks(times, values, params = {}) {
295
+ const {
296
+ threshold,
297
+ minDistance,
298
+ preMax,
299
+ postMax
300
+ } = { ...DEFAULT_PEAK_PICKING_PARAMS, ...params };
301
+ if (times.length === 0 || values.length === 0) {
302
+ return {
303
+ times: new Float32Array(0),
304
+ strengths: new Float32Array(0)
305
+ };
306
+ }
307
+ let minVal = values[0];
308
+ let maxVal = values[0];
309
+ for (let i = 1; i < values.length; i++) {
310
+ const v = values[i];
311
+ if (v < minVal) minVal = v;
312
+ if (v > maxVal) maxVal = v;
313
+ }
314
+ const range = maxVal - minVal;
315
+ if (range <= 0) {
316
+ return {
317
+ times: new Float32Array(0),
318
+ strengths: new Float32Array(0)
319
+ };
320
+ }
321
+ const normalize = (v) => (v - minVal) / range;
322
+ const peakIndices = [];
323
+ const peakStrengths = [];
324
+ for (let i = preMax; i < values.length - postMax; i++) {
325
+ const currentVal = values[i];
326
+ const normalizedVal = normalize(currentVal);
327
+ if (normalizedVal < threshold) continue;
328
+ let isMax = true;
329
+ for (let j = 1; j <= preMax; j++) {
330
+ if (values[i - j] >= currentVal) {
331
+ isMax = false;
332
+ break;
333
+ }
334
+ }
335
+ if (isMax) {
336
+ for (let j = 1; j <= postMax; j++) {
337
+ if (values[i + j] > currentVal) {
338
+ isMax = false;
339
+ break;
340
+ }
341
+ }
342
+ }
343
+ if (isMax) {
344
+ peakIndices.push(i);
345
+ peakStrengths.push(normalizedVal);
346
+ }
347
+ }
348
+ const filteredIndices = [];
349
+ const filteredStrengths = [];
350
+ for (let i = 0; i < peakIndices.length; i++) {
351
+ const idx = peakIndices[i];
352
+ const time = times[idx];
353
+ const strength = peakStrengths[i];
354
+ let shouldKeep = true;
355
+ for (let j = 0; j < filteredIndices.length; j++) {
356
+ const prevIdx = filteredIndices[j];
357
+ const prevTime = times[prevIdx];
358
+ const prevStrength = filteredStrengths[j];
359
+ if (Math.abs(time - prevTime) < minDistance) {
360
+ if (strength > prevStrength) {
361
+ filteredIndices[j] = idx;
362
+ filteredStrengths[j] = strength;
363
+ }
364
+ shouldKeep = false;
365
+ break;
366
+ }
367
+ }
368
+ if (shouldKeep) {
369
+ filteredIndices.push(idx);
370
+ filteredStrengths.push(strength);
371
+ }
372
+ }
373
+ const resultTimes = new Float32Array(filteredIndices.length);
374
+ const resultStrengths = new Float32Array(filteredStrengths.length);
375
+ for (let i = 0; i < filteredIndices.length; i++) {
376
+ resultTimes[i] = times[filteredIndices[i]];
377
+ resultStrengths[i] = filteredStrengths[i];
378
+ }
379
+ return {
380
+ times: resultTimes,
381
+ strengths: resultStrengths
382
+ };
383
+ }
384
+ function computeAdaptiveThreshold(values, windowSize = 20, thresholdMultiplier = 1.5) {
385
+ if (values.length === 0) {
386
+ return new Float32Array(0);
387
+ }
388
+ let minVal = values[0];
389
+ let maxVal = values[0];
390
+ for (let i = 1; i < values.length; i++) {
391
+ const v = values[i];
392
+ if (v < minVal) minVal = v;
393
+ if (v > maxVal) maxVal = v;
394
+ }
395
+ const range = maxVal - minVal;
396
+ if (range <= 0) {
397
+ return new Float32Array(values.length).fill(0.5);
398
+ }
399
+ const halfWindow = Math.floor(windowSize / 2);
400
+ const thresholdCurve = new Float32Array(values.length);
401
+ for (let i = 0; i < values.length; i++) {
402
+ const start = Math.max(0, i - halfWindow);
403
+ const end = Math.min(values.length, i + halfWindow + 1);
404
+ const windowLen = end - start;
405
+ let sum = 0;
406
+ for (let j = start; j < end; j++) {
407
+ sum += values[j];
408
+ }
409
+ const mean = sum / windowLen;
410
+ let sumSq = 0;
411
+ for (let j = start; j < end; j++) {
412
+ const diff = values[j] - mean;
413
+ sumSq += diff * diff;
414
+ }
415
+ const std = Math.sqrt(sumSq / windowLen);
416
+ const adaptiveThresh = mean + thresholdMultiplier * std;
417
+ thresholdCurve[i] = Math.max(0, Math.min(1, (adaptiveThresh - minVal) / range));
418
+ }
419
+ return thresholdCurve;
420
+ }
421
+ function pickPeaksAdaptive(times, values, windowSize = 20, params = {}, includeThresholdCurve = false) {
422
+ const {
423
+ threshold,
424
+ minDistance,
425
+ preMax,
426
+ postMax
427
+ } = { ...DEFAULT_PEAK_PICKING_PARAMS, threshold: 1.5, ...params };
428
+ if (times.length === 0 || values.length === 0) {
429
+ return {
430
+ times: new Float32Array(0),
431
+ strengths: new Float32Array(0)
432
+ };
433
+ }
434
+ const adaptiveThreshold = new Float32Array(values.length);
435
+ const halfWindow = Math.floor(windowSize / 2);
436
+ for (let i = 0; i < values.length; i++) {
437
+ const start = Math.max(0, i - halfWindow);
438
+ const end = Math.min(values.length, i + halfWindow + 1);
439
+ const windowLen = end - start;
440
+ let sum = 0;
441
+ for (let j = start; j < end; j++) {
442
+ sum += values[j];
443
+ }
444
+ const mean = sum / windowLen;
445
+ let sumSq = 0;
446
+ for (let j = start; j < end; j++) {
447
+ const diff = values[j] - mean;
448
+ sumSq += diff * diff;
449
+ }
450
+ const std = Math.sqrt(sumSq / windowLen);
451
+ adaptiveThreshold[i] = mean + threshold * std;
452
+ }
453
+ let minVal = values[0];
454
+ let maxVal = values[0];
455
+ for (let i = 1; i < values.length; i++) {
456
+ const v = values[i];
457
+ if (v < minVal) minVal = v;
458
+ if (v > maxVal) maxVal = v;
459
+ }
460
+ const range = maxVal - minVal;
461
+ const normalize = range > 0 ? (v) => (v - minVal) / range : () => 0.5;
462
+ const peakIndices = [];
463
+ const peakStrengths = [];
464
+ for (let i = preMax; i < values.length - postMax; i++) {
465
+ const currentVal = values[i];
466
+ if (currentVal < adaptiveThreshold[i]) continue;
467
+ let isMax = true;
468
+ for (let j = 1; j <= preMax; j++) {
469
+ if (values[i - j] >= currentVal) {
470
+ isMax = false;
471
+ break;
472
+ }
473
+ }
474
+ if (isMax) {
475
+ for (let j = 1; j <= postMax; j++) {
476
+ if (values[i + j] > currentVal) {
477
+ isMax = false;
478
+ break;
479
+ }
480
+ }
481
+ }
482
+ if (isMax) {
483
+ peakIndices.push(i);
484
+ peakStrengths.push(normalize(currentVal));
485
+ }
486
+ }
487
+ const filteredIndices = [];
488
+ const filteredStrengths = [];
489
+ for (let i = 0; i < peakIndices.length; i++) {
490
+ const idx = peakIndices[i];
491
+ const time = times[idx];
492
+ const strength = peakStrengths[i];
493
+ let shouldKeep = true;
494
+ for (let j = 0; j < filteredIndices.length; j++) {
495
+ const prevIdx = filteredIndices[j];
496
+ const prevTime = times[prevIdx];
497
+ const prevStrength = filteredStrengths[j];
498
+ if (Math.abs(time - prevTime) < minDistance) {
499
+ if (strength > prevStrength) {
500
+ filteredIndices[j] = idx;
501
+ filteredStrengths[j] = strength;
502
+ }
503
+ shouldKeep = false;
504
+ break;
505
+ }
506
+ }
507
+ if (shouldKeep) {
508
+ filteredIndices.push(idx);
509
+ filteredStrengths.push(strength);
510
+ }
511
+ }
512
+ const resultTimes = new Float32Array(filteredIndices.length);
513
+ const resultStrengths = new Float32Array(filteredStrengths.length);
514
+ for (let i = 0; i < filteredIndices.length; i++) {
515
+ resultTimes[i] = times[filteredIndices[i]];
516
+ resultStrengths[i] = filteredStrengths[i];
517
+ }
518
+ const baseResult = {
519
+ times: resultTimes,
520
+ strengths: resultStrengths
521
+ };
522
+ if (!includeThresholdCurve) {
523
+ return baseResult;
524
+ }
525
+ const normalizedThreshold = new Float32Array(values.length);
526
+ for (let i = 0; i < values.length; i++) {
527
+ normalizedThreshold[i] = range > 0 ? Math.max(0, Math.min(1, (adaptiveThreshold[i] - minVal) / range)) : 0.5;
528
+ }
529
+ return {
530
+ ...baseResult,
531
+ thresholdCurve: normalizedThreshold,
532
+ thresholdTimes: times
533
+ };
534
+ }
535
+ function applyHysteresisGate(times, values, peakTimes, peakStrengths, params) {
536
+ const { onThreshold, offThreshold, minDistance } = params;
537
+ if (peakTimes.length === 0) {
538
+ return { times: new Float32Array(0), strengths: new Float32Array(0) };
539
+ }
540
+ let minVal = values[0];
541
+ let maxVal = values[0];
542
+ for (let i = 1; i < values.length; i++) {
543
+ const v = values[i];
544
+ if (v < minVal) minVal = v;
545
+ if (v > maxVal) maxVal = v;
546
+ }
547
+ const range = maxVal - minVal;
548
+ const normalize = range > 0 ? (v) => (v - minVal) / range : () => 0.5;
549
+ const getIndexForTime = (t) => {
550
+ let lo = 0;
551
+ let hi = times.length - 1;
552
+ while (lo < hi) {
553
+ const mid = Math.floor((lo + hi) / 2);
554
+ if (times[mid] < t) {
555
+ lo = mid + 1;
556
+ } else {
557
+ hi = mid;
558
+ }
559
+ }
560
+ return lo;
561
+ };
562
+ const filteredTimes = [];
563
+ const filteredStrengths = [];
564
+ let lastPeakTime = -Infinity;
565
+ let gateOpen = true;
566
+ for (let i = 0; i < peakTimes.length; i++) {
567
+ const peakTime = peakTimes[i];
568
+ const peakStrength = peakStrengths[i];
569
+ if (peakTime - lastPeakTime < minDistance) {
570
+ continue;
571
+ }
572
+ if (!gateOpen) {
573
+ const startIdx = getIndexForTime(lastPeakTime);
574
+ const endIdx = getIndexForTime(peakTime);
575
+ for (let j = startIdx; j < endIdx && j < values.length; j++) {
576
+ if (normalize(values[j]) < offThreshold) {
577
+ gateOpen = true;
578
+ break;
579
+ }
580
+ }
581
+ }
582
+ if (gateOpen && peakStrength >= onThreshold) {
583
+ filteredTimes.push(peakTime);
584
+ filteredStrengths.push(peakStrength);
585
+ lastPeakTime = peakTime;
586
+ gateOpen = false;
587
+ }
588
+ }
589
+ return {
590
+ times: new Float32Array(filteredTimes),
591
+ strengths: new Float32Array(filteredStrengths)
592
+ };
593
+ }
594
+
595
+ // src/dsp/musicalTime.ts
596
+ function findSegmentAtTime(time, segments) {
597
+ for (const segment of segments) {
598
+ if (time >= segment.startTime && time < segment.endTime) {
599
+ return segment;
600
+ }
601
+ }
602
+ return null;
603
+ }
604
+ function computeBeatPosition(time, segments) {
605
+ const segment = findSegmentAtTime(time, segments);
606
+ if (!segment) {
607
+ return null;
608
+ }
609
+ const period = 60 / segment.bpm;
610
+ const beatsFromStart = (time - segment.phaseOffset) / period;
611
+ const beatIndex = Math.floor(beatsFromStart);
612
+ const beatPhase = beatsFromStart - beatIndex;
613
+ return {
614
+ segmentId: segment.id,
615
+ beatIndex,
616
+ beatPhase,
617
+ beatPosition: beatsFromStart,
618
+ bpm: segment.bpm
619
+ };
620
+ }
621
+ function computeBeatPositionFromStructure(time, structure) {
622
+ if (!structure || structure.segments.length === 0) {
623
+ return null;
624
+ }
625
+ return computeBeatPosition(time, structure.segments);
626
+ }
627
+ function generateSegmentId() {
628
+ return `seg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
629
+ }
630
+ function createSegmentFromGrid(grid, startTime, endTime) {
631
+ const effectivePhaseOffset = grid.phaseOffset + grid.userNudge;
632
+ return {
633
+ id: generateSegmentId(),
634
+ bpm: grid.bpm,
635
+ phaseOffset: effectivePhaseOffset,
636
+ startTime,
637
+ endTime,
638
+ confidence: grid.confidence,
639
+ provenance: {
640
+ source: "promoted_from_hypothesis",
641
+ sourceHypothesisId: grid.sourceHypothesisId,
642
+ promotedAt: (/* @__PURE__ */ new Date()).toISOString(),
643
+ userNudge: grid.userNudge
644
+ }
645
+ };
646
+ }
647
+ function createMusicalTimeStructure() {
648
+ const now = (/* @__PURE__ */ new Date()).toISOString();
649
+ return {
650
+ version: 1,
651
+ segments: [],
652
+ createdAt: now,
653
+ modifiedAt: now
654
+ };
655
+ }
656
+ function validateSegments(segments) {
657
+ const errors = [];
658
+ for (let i = 0; i < segments.length; i++) {
659
+ const seg = segments[i];
660
+ if (!seg) continue;
661
+ if (seg.startTime >= seg.endTime) {
662
+ errors.push(`Segment ${seg.id}: startTime (${seg.startTime}) >= endTime (${seg.endTime})`);
663
+ }
664
+ if (seg.bpm <= 0) {
665
+ errors.push(`Segment ${seg.id}: bpm (${seg.bpm}) must be positive`);
666
+ }
667
+ if (i < segments.length - 1) {
668
+ const next = segments[i + 1];
669
+ if (!next) continue;
670
+ if (seg.startTime >= next.startTime) {
671
+ errors.push(`Segment ${seg.id} not ordered before ${next.id}`);
672
+ }
673
+ if (seg.endTime > next.startTime) {
674
+ errors.push(`Segment ${seg.id} overlaps with ${next.id}`);
675
+ }
676
+ }
677
+ }
678
+ return errors;
679
+ }
680
+ function sortSegments(segments) {
681
+ return [...segments].sort((a, b) => a.startTime - b.startTime);
682
+ }
683
+ function splitSegment(segment, splitTime) {
684
+ if (splitTime <= segment.startTime || splitTime >= segment.endTime) {
685
+ throw new Error(
686
+ `Split time ${splitTime} must be within segment bounds [${segment.startTime}, ${segment.endTime})`
687
+ );
688
+ }
689
+ const beforeSegment = {
690
+ ...segment,
691
+ id: generateSegmentId(),
692
+ endTime: splitTime
693
+ };
694
+ const afterSegment = {
695
+ ...segment,
696
+ id: generateSegmentId(),
697
+ startTime: splitTime
698
+ // phaseOffset stays the same - beat grid continues seamlessly
699
+ };
700
+ return [beforeSegment, afterSegment];
701
+ }
702
+ function generateSegmentBeatTimes(segment) {
703
+ const period = 60 / segment.bpm;
704
+ const times = [];
705
+ const beatsBeforeStart = Math.ceil((segment.startTime - segment.phaseOffset) / period);
706
+ let time = segment.phaseOffset + beatsBeforeStart * period;
707
+ while (time < segment.endTime) {
708
+ if (time >= segment.startTime) {
709
+ times.push(time);
710
+ }
711
+ time += period;
712
+ }
713
+ return times;
714
+ }
715
+
716
+ // src/dsp/frequencyBand.ts
717
+ function generateBandId() {
718
+ return `band-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
719
+ }
720
+ function validateFrequencySegments(segments) {
721
+ const errors = [];
722
+ for (let i = 0; i < segments.length; i++) {
723
+ const seg = segments[i];
724
+ if (!seg) continue;
725
+ if (seg.lowHzStart < 0 || seg.highHzStart < 0) {
726
+ errors.push(`Segment ${i}: start frequencies must be non-negative`);
727
+ }
728
+ if (seg.lowHzStart >= seg.highHzStart) {
729
+ errors.push(
730
+ `Segment ${i}: lowHzStart (${seg.lowHzStart}) must be < highHzStart (${seg.highHzStart})`
731
+ );
732
+ }
733
+ if (seg.lowHzEnd < 0 || seg.highHzEnd < 0) {
734
+ errors.push(`Segment ${i}: end frequencies must be non-negative`);
735
+ }
736
+ if (seg.lowHzEnd >= seg.highHzEnd) {
737
+ errors.push(
738
+ `Segment ${i}: lowHzEnd (${seg.lowHzEnd}) must be < highHzEnd (${seg.highHzEnd})`
739
+ );
740
+ }
741
+ if (seg.startTime >= seg.endTime) {
742
+ errors.push(
743
+ `Segment ${i}: startTime (${seg.startTime}) must be < endTime (${seg.endTime})`
744
+ );
745
+ }
746
+ if (i < segments.length - 1) {
747
+ const next = segments[i + 1];
748
+ if (!next) continue;
749
+ if (seg.endTime > next.startTime) {
750
+ errors.push(`Segment ${i} overlaps with segment ${i + 1}`);
751
+ }
752
+ if (seg.startTime >= next.startTime) {
753
+ errors.push(`Segment ${i} not ordered before segment ${i + 1}`);
754
+ }
755
+ }
756
+ }
757
+ return errors;
758
+ }
759
+ function validateFrequencyBand(band) {
760
+ const errors = [];
761
+ if (!band.id || band.id.trim() === "") {
762
+ errors.push("Band must have a non-empty id");
763
+ }
764
+ if (!band.label || band.label.trim() === "") {
765
+ errors.push("Band must have a non-empty label");
766
+ }
767
+ if (band.frequencyShape.length === 0) {
768
+ errors.push(`Band "${band.label}": must have at least one frequency segment`);
769
+ }
770
+ const segmentErrors = validateFrequencySegments(band.frequencyShape);
771
+ errors.push(...segmentErrors.map((e) => `Band "${band.label}": ${e}`));
772
+ if (band.timeScope.kind === "sectioned" && band.frequencyShape.length > 0) {
773
+ const { startTime, endTime } = band.timeScope;
774
+ const first = band.frequencyShape[0];
775
+ const last = band.frequencyShape[band.frequencyShape.length - 1];
776
+ if (first && first.startTime > startTime) {
777
+ errors.push(
778
+ `Band "${band.label}": segments don't cover scope start (first segment starts at ${first.startTime}, scope starts at ${startTime})`
779
+ );
780
+ }
781
+ if (last && last.endTime < endTime) {
782
+ errors.push(
783
+ `Band "${band.label}": segments don't cover scope end (last segment ends at ${last.endTime}, scope ends at ${endTime})`
784
+ );
785
+ }
786
+ }
787
+ return errors;
788
+ }
789
+ function validateBandStructure(structure) {
790
+ const errors = [];
791
+ const ids = /* @__PURE__ */ new Set();
792
+ for (const band of structure.bands) {
793
+ if (ids.has(band.id)) {
794
+ errors.push(`Duplicate band ID: ${band.id}`);
795
+ }
796
+ ids.add(band.id);
797
+ }
798
+ for (const band of structure.bands) {
799
+ errors.push(...validateFrequencyBand(band));
800
+ }
801
+ return errors;
802
+ }
803
+ function createBandStructure() {
804
+ const now = (/* @__PURE__ */ new Date()).toISOString();
805
+ return {
806
+ version: 2,
807
+ bands: [],
808
+ createdAt: now,
809
+ modifiedAt: now
810
+ };
811
+ }
812
+ function createConstantBand(label, lowHz, highHz, duration, options) {
813
+ const now = (/* @__PURE__ */ new Date()).toISOString();
814
+ return {
815
+ id: options?.id ?? generateBandId(),
816
+ label,
817
+ sourceId: options?.sourceId ?? "mixdown",
818
+ enabled: options?.enabled ?? true,
819
+ timeScope: { kind: "global" },
820
+ frequencyShape: [
821
+ {
822
+ startTime: 0,
823
+ endTime: duration,
824
+ lowHzStart: lowHz,
825
+ highHzStart: highHz,
826
+ lowHzEnd: lowHz,
827
+ highHzEnd: highHz
828
+ }
829
+ ],
830
+ sortOrder: options?.sortOrder ?? 0,
831
+ provenance: {
832
+ source: "manual",
833
+ createdAt: now
834
+ }
835
+ };
836
+ }
837
+ function createSectionedBand(label, lowHz, highHz, startTime, endTime, options) {
838
+ const now = (/* @__PURE__ */ new Date()).toISOString();
839
+ return {
840
+ id: options?.id ?? generateBandId(),
841
+ label,
842
+ sourceId: options?.sourceId ?? "mixdown",
843
+ enabled: options?.enabled ?? true,
844
+ timeScope: { kind: "sectioned", startTime, endTime },
845
+ frequencyShape: [
846
+ {
847
+ startTime,
848
+ endTime,
849
+ lowHzStart: lowHz,
850
+ highHzStart: highHz,
851
+ lowHzEnd: lowHz,
852
+ highHzEnd: highHz
853
+ }
854
+ ],
855
+ sortOrder: options?.sortOrder ?? 0,
856
+ provenance: {
857
+ source: "manual",
858
+ createdAt: now
859
+ }
860
+ };
861
+ }
862
+ function createStandardBands(duration, sourceId = "mixdown") {
863
+ const now = (/* @__PURE__ */ new Date()).toISOString();
864
+ const bands = [
865
+ { label: "Sub Bass", lowHz: 20, highHz: 60 },
866
+ { label: "Bass", lowHz: 60, highHz: 250 },
867
+ { label: "Low Mids", lowHz: 250, highHz: 500 },
868
+ { label: "Mids", lowHz: 500, highHz: 2e3 },
869
+ { label: "High Mids", lowHz: 2e3, highHz: 4e3 },
870
+ { label: "Highs", lowHz: 4e3, highHz: 2e4 }
871
+ ];
872
+ return bands.map((b, index) => ({
873
+ id: generateBandId(),
874
+ label: b.label,
875
+ sourceId,
876
+ enabled: true,
877
+ timeScope: { kind: "global" },
878
+ frequencyShape: [
879
+ {
880
+ startTime: 0,
881
+ endTime: duration,
882
+ lowHzStart: b.lowHz,
883
+ highHzStart: b.highHz,
884
+ lowHzEnd: b.lowHz,
885
+ highHzEnd: b.highHz
886
+ }
887
+ ],
888
+ sortOrder: index,
889
+ provenance: {
890
+ source: "preset",
891
+ createdAt: now,
892
+ presetName: "Standard 6-Band"
893
+ }
894
+ }));
895
+ }
896
+ function bandsActiveAt(structure, time, sourceId) {
897
+ if (!structure) return [];
898
+ return structure.bands.filter((band) => {
899
+ if (!band.enabled) return false;
900
+ if (sourceId !== void 0 && band.sourceId !== sourceId) return false;
901
+ if (band.timeScope.kind === "global") return true;
902
+ return time >= band.timeScope.startTime && time < band.timeScope.endTime;
903
+ });
904
+ }
905
+ function frequencyBoundsAt(band, time) {
906
+ if (!band.enabled) return null;
907
+ if (band.timeScope.kind === "sectioned") {
908
+ if (time < band.timeScope.startTime || time >= band.timeScope.endTime) {
909
+ return null;
910
+ }
911
+ }
912
+ for (const seg of band.frequencyShape) {
913
+ if (time >= seg.startTime && time < seg.endTime) {
914
+ const t = (time - seg.startTime) / (seg.endTime - seg.startTime);
915
+ return {
916
+ bandId: band.id,
917
+ lowHz: seg.lowHzStart + (seg.lowHzEnd - seg.lowHzStart) * t,
918
+ highHz: seg.highHzStart + (seg.highHzEnd - seg.highHzStart) * t,
919
+ enabled: band.enabled
920
+ };
921
+ }
922
+ }
923
+ const last = band.frequencyShape[band.frequencyShape.length - 1];
924
+ if (last && Math.abs(time - last.endTime) < 1e-3) {
925
+ return {
926
+ bandId: band.id,
927
+ lowHz: last.lowHzEnd,
928
+ highHz: last.highHzEnd,
929
+ enabled: band.enabled
930
+ };
931
+ }
932
+ return null;
933
+ }
934
+ function allFrequencyBoundsAt(structure, time) {
935
+ if (!structure) return [];
936
+ const bounds = [];
937
+ for (const band of structure.bands) {
938
+ const b = frequencyBoundsAt(band, time);
939
+ if (b) bounds.push(b);
940
+ }
941
+ bounds.sort((a, b) => {
942
+ const bandA = structure.bands.find((band) => band.id === a.bandId);
943
+ const bandB = structure.bands.find((band) => band.id === b.bandId);
944
+ return (bandA?.sortOrder ?? 0) - (bandB?.sortOrder ?? 0);
945
+ });
946
+ return bounds;
947
+ }
948
+ function findBandById(structure, id) {
949
+ if (!structure) return null;
950
+ return structure.bands.find((b) => b.id === id) ?? null;
951
+ }
952
+ function sortBands(bands) {
953
+ return [...bands].sort((a, b) => a.sortOrder - b.sortOrder);
954
+ }
955
+ function sortFrequencySegments(segments) {
956
+ return [...segments].sort((a, b) => a.startTime - b.startTime);
957
+ }
958
+ function touchStructure(structure) {
959
+ return {
960
+ ...structure,
961
+ modifiedAt: (/* @__PURE__ */ new Date()).toISOString()
962
+ };
963
+ }
964
+ function addBandToStructure(structure, band) {
965
+ return {
966
+ ...structure,
967
+ bands: sortBands([...structure.bands, band]),
968
+ modifiedAt: (/* @__PURE__ */ new Date()).toISOString()
969
+ };
970
+ }
971
+ function removeBandFromStructure(structure, bandId) {
972
+ return {
973
+ ...structure,
974
+ bands: structure.bands.filter((b) => b.id !== bandId),
975
+ modifiedAt: (/* @__PURE__ */ new Date()).toISOString()
976
+ };
977
+ }
978
+ function updateBandInStructure(structure, bandId, updates) {
979
+ return {
980
+ ...structure,
981
+ bands: sortBands(
982
+ structure.bands.map((b) => b.id === bandId ? { ...b, ...updates } : b)
983
+ ),
984
+ modifiedAt: (/* @__PURE__ */ new Date()).toISOString()
985
+ };
986
+ }
987
+ function keyframesFromBand(band) {
988
+ const keyframes = [];
989
+ const segments = sortFrequencySegments(band.frequencyShape);
990
+ for (let i = 0; i < segments.length; i++) {
991
+ const seg = segments[i];
992
+ if (!seg) continue;
993
+ keyframes.push({
994
+ time: seg.startTime,
995
+ lowHz: seg.lowHzStart,
996
+ highHz: seg.highHzStart,
997
+ segmentIndex: i,
998
+ edge: "start"
999
+ });
1000
+ const nextSeg = segments[i + 1];
1001
+ const isShared = nextSeg && Math.abs(nextSeg.startTime - seg.endTime) < 1e-3;
1002
+ if (!isShared) {
1003
+ keyframes.push({
1004
+ time: seg.endTime,
1005
+ lowHz: seg.lowHzEnd,
1006
+ highHz: seg.highHzEnd,
1007
+ segmentIndex: i,
1008
+ edge: "end"
1009
+ });
1010
+ }
1011
+ }
1012
+ return keyframes;
1013
+ }
1014
+ function segmentsFromKeyframes(keyframes) {
1015
+ if (keyframes.length < 2) return [];
1016
+ const sorted = [...keyframes].sort((a, b) => a.time - b.time);
1017
+ const segments = [];
1018
+ for (let i = 0; i < sorted.length - 1; i++) {
1019
+ const curr = sorted[i];
1020
+ const next = sorted[i + 1];
1021
+ if (!curr || !next) continue;
1022
+ segments.push({
1023
+ startTime: curr.time,
1024
+ endTime: next.time,
1025
+ lowHzStart: curr.lowHz,
1026
+ highHzStart: curr.highHz,
1027
+ lowHzEnd: next.lowHz,
1028
+ highHzEnd: next.highHz
1029
+ });
1030
+ }
1031
+ return segments;
1032
+ }
1033
+ function splitBandSegmentAt(band, time) {
1034
+ const segments = sortFrequencySegments(band.frequencyShape);
1035
+ const segmentIndex = segments.findIndex(
1036
+ (seg2) => time > seg2.startTime && time < seg2.endTime
1037
+ );
1038
+ if (segmentIndex === -1) {
1039
+ return band;
1040
+ }
1041
+ const seg = segments[segmentIndex];
1042
+ if (!seg) return band;
1043
+ const t = (time - seg.startTime) / (seg.endTime - seg.startTime);
1044
+ const lowHz = seg.lowHzStart + (seg.lowHzEnd - seg.lowHzStart) * t;
1045
+ const highHz = seg.highHzStart + (seg.highHzEnd - seg.highHzStart) * t;
1046
+ const firstHalf = {
1047
+ startTime: seg.startTime,
1048
+ endTime: time,
1049
+ lowHzStart: seg.lowHzStart,
1050
+ highHzStart: seg.highHzStart,
1051
+ lowHzEnd: lowHz,
1052
+ highHzEnd: highHz
1053
+ };
1054
+ const secondHalf = {
1055
+ startTime: time,
1056
+ endTime: seg.endTime,
1057
+ lowHzStart: lowHz,
1058
+ highHzStart: highHz,
1059
+ lowHzEnd: seg.lowHzEnd,
1060
+ highHzEnd: seg.highHzEnd
1061
+ };
1062
+ const newSegments = [
1063
+ ...segments.slice(0, segmentIndex),
1064
+ firstHalf,
1065
+ secondHalf,
1066
+ ...segments.slice(segmentIndex + 1)
1067
+ ];
1068
+ return {
1069
+ ...band,
1070
+ frequencyShape: newSegments
1071
+ };
1072
+ }
1073
+ function mergeAdjacentSegments(band, tolerance = 0.1) {
1074
+ const segments = sortFrequencySegments(band.frequencyShape);
1075
+ if (segments.length < 2) return band;
1076
+ const merged = [];
1077
+ let current = segments[0];
1078
+ if (!current) return band;
1079
+ for (let i = 1; i < segments.length; i++) {
1080
+ const next = segments[i];
1081
+ if (!next) continue;
1082
+ const timeMatches = Math.abs(current.endTime - next.startTime) < 1e-3;
1083
+ const lowMatches = Math.abs(current.lowHzEnd - next.lowHzStart) < tolerance;
1084
+ const highMatches = Math.abs(current.highHzEnd - next.highHzStart) < tolerance;
1085
+ if (timeMatches && lowMatches && highMatches) {
1086
+ current = {
1087
+ startTime: current.startTime,
1088
+ endTime: next.endTime,
1089
+ lowHzStart: current.lowHzStart,
1090
+ highHzStart: current.highHzStart,
1091
+ lowHzEnd: next.lowHzEnd,
1092
+ highHzEnd: next.highHzEnd
1093
+ };
1094
+ } else {
1095
+ merged.push(current);
1096
+ current = next;
1097
+ }
1098
+ }
1099
+ merged.push(current);
1100
+ return {
1101
+ ...band,
1102
+ frequencyShape: merged
1103
+ };
1104
+ }
1105
+ function removeKeyframe(band, time) {
1106
+ const segments = sortFrequencySegments(band.frequencyShape);
1107
+ if (segments.length < 2) return band;
1108
+ const endingIndex = segments.findIndex(
1109
+ (seg) => Math.abs(seg.endTime - time) < 1e-3
1110
+ );
1111
+ const startingIndex = segments.findIndex(
1112
+ (seg) => Math.abs(seg.startTime - time) < 1e-3
1113
+ );
1114
+ if (endingIndex === -1 || startingIndex === -1) return band;
1115
+ if (endingIndex !== startingIndex - 1) return band;
1116
+ const endingSeg = segments[endingIndex];
1117
+ const startingSeg = segments[startingIndex];
1118
+ if (!endingSeg || !startingSeg) return band;
1119
+ const merged = {
1120
+ startTime: endingSeg.startTime,
1121
+ endTime: startingSeg.endTime,
1122
+ lowHzStart: endingSeg.lowHzStart,
1123
+ highHzStart: endingSeg.highHzStart,
1124
+ lowHzEnd: startingSeg.lowHzEnd,
1125
+ highHzEnd: startingSeg.highHzEnd
1126
+ };
1127
+ const newSegments = [
1128
+ ...segments.slice(0, endingIndex),
1129
+ merged,
1130
+ ...segments.slice(startingIndex + 1)
1131
+ ];
1132
+ return {
1133
+ ...band,
1134
+ frequencyShape: newSegments
1135
+ };
1136
+ }
1137
+ function updateKeyframe(band, time, lowHz, highHz) {
1138
+ const segments = band.frequencyShape.map((seg) => {
1139
+ const isStart = Math.abs(seg.startTime - time) < 1e-3;
1140
+ const isEnd = Math.abs(seg.endTime - time) < 1e-3;
1141
+ if (!isStart && !isEnd) return seg;
1142
+ const newSeg = { ...seg };
1143
+ if (isStart) {
1144
+ if (lowHz !== void 0) newSeg.lowHzStart = lowHz;
1145
+ if (highHz !== void 0) newSeg.highHzStart = highHz;
1146
+ }
1147
+ if (isEnd) {
1148
+ if (lowHz !== void 0) newSeg.lowHzEnd = lowHz;
1149
+ if (highHz !== void 0) newSeg.highHzEnd = highHz;
1150
+ }
1151
+ return newSeg;
1152
+ });
1153
+ return {
1154
+ ...band,
1155
+ frequencyShape: segments
1156
+ };
1157
+ }
1158
+ function moveKeyframeTime(band, oldTime, newTime) {
1159
+ const segments = sortFrequencySegments(band.frequencyShape);
1160
+ const endingIndex = segments.findIndex(
1161
+ (seg) => Math.abs(seg.endTime - oldTime) < 1e-3
1162
+ );
1163
+ const startingIndex = segments.findIndex(
1164
+ (seg) => Math.abs(seg.startTime - oldTime) < 1e-3
1165
+ );
1166
+ if (endingIndex === -1 || startingIndex === -1) return band;
1167
+ if (endingIndex !== startingIndex - 1) return band;
1168
+ const endingSeg = segments[endingIndex];
1169
+ const startingSeg = segments[startingIndex];
1170
+ if (!endingSeg || !startingSeg) return band;
1171
+ if (newTime <= endingSeg.startTime || newTime >= startingSeg.endTime) {
1172
+ return band;
1173
+ }
1174
+ const newSegments = segments.map((seg, i) => {
1175
+ if (i === endingIndex) {
1176
+ return { ...seg, endTime: newTime };
1177
+ }
1178
+ if (i === startingIndex) {
1179
+ return { ...seg, startTime: newTime };
1180
+ }
1181
+ return seg;
1182
+ });
1183
+ return {
1184
+ ...band,
1185
+ frequencyShape: newSegments
1186
+ };
1187
+ }
1188
+
1189
+ // src/dsp/bandMask.ts
1190
+ function binToHz(bin, sampleRate, fftSize) {
1191
+ return bin * (sampleRate / fftSize);
1192
+ }
1193
+ function hzToBin(hz, sampleRate, fftSize) {
1194
+ return hz / (sampleRate / fftSize);
1195
+ }
1196
+ function computeBandMaskAtTime(band, time, sampleRate, fftSize, options) {
1197
+ const bounds = frequencyBoundsAt(band, time);
1198
+ if (!bounds) return null;
1199
+ const nBins = (fftSize >>> 1) + 1;
1200
+ const mask = new Float32Array(nBins);
1201
+ const edgeSmoothHz = options?.edgeSmoothHz ?? 0;
1202
+ for (let k = 0; k < nBins; k++) {
1203
+ const hz = binToHz(k, sampleRate, fftSize);
1204
+ if (hz < bounds.lowHz || hz > bounds.highHz) {
1205
+ mask[k] = 0;
1206
+ } else if (edgeSmoothHz <= 0) {
1207
+ mask[k] = 1;
1208
+ } else {
1209
+ const distFromLow = hz - bounds.lowHz;
1210
+ const distFromHigh = bounds.highHz - hz;
1211
+ let gain = 1;
1212
+ if (distFromLow < edgeSmoothHz) {
1213
+ gain *= 0.5 * (1 - Math.cos(Math.PI * distFromLow / edgeSmoothHz));
1214
+ }
1215
+ if (distFromHigh < edgeSmoothHz) {
1216
+ gain *= 0.5 * (1 - Math.cos(Math.PI * distFromHigh / edgeSmoothHz));
1217
+ }
1218
+ mask[k] = gain;
1219
+ }
1220
+ }
1221
+ return mask;
1222
+ }
1223
+ function applyBandMaskToSpectrogram(spec, band, options) {
1224
+ const nFrames = spec.times.length;
1225
+ const nBins = (spec.fftSize >>> 1) + 1;
1226
+ const maskedMagnitudes = new Array(nFrames);
1227
+ const energyRetained = new Float32Array(nFrames);
1228
+ for (let t = 0; t < nFrames; t++) {
1229
+ const time = spec.times[t] ?? 0;
1230
+ const srcMags = spec.magnitudes[t];
1231
+ if (!srcMags) {
1232
+ maskedMagnitudes[t] = new Float32Array(nBins);
1233
+ energyRetained[t] = 0;
1234
+ continue;
1235
+ }
1236
+ const mask = computeBandMaskAtTime(
1237
+ band,
1238
+ time,
1239
+ spec.sampleRate,
1240
+ spec.fftSize,
1241
+ options
1242
+ );
1243
+ if (!mask) {
1244
+ maskedMagnitudes[t] = new Float32Array(nBins);
1245
+ energyRetained[t] = 0;
1246
+ continue;
1247
+ }
1248
+ const masked = new Float32Array(nBins);
1249
+ let originalEnergy = 0;
1250
+ let maskedEnergy = 0;
1251
+ for (let k = 0; k < nBins; k++) {
1252
+ const mag = srcMags[k] ?? 0;
1253
+ const maskedMag = mag * (mask[k] ?? 0);
1254
+ masked[k] = maskedMag;
1255
+ originalEnergy += mag * mag;
1256
+ maskedEnergy += maskedMag * maskedMag;
1257
+ }
1258
+ maskedMagnitudes[t] = masked;
1259
+ energyRetained[t] = originalEnergy > 0 ? maskedEnergy / originalEnergy : 0;
1260
+ }
1261
+ return {
1262
+ sampleRate: spec.sampleRate,
1263
+ fftSize: spec.fftSize,
1264
+ hopSize: spec.hopSize,
1265
+ times: spec.times,
1266
+ magnitudes: maskedMagnitudes,
1267
+ bandId: band.id,
1268
+ energyRetainedPerFrame: energyRetained
1269
+ };
1270
+ }
1271
+ function computeFrameEnergy(magnitudes) {
1272
+ let energy = 0;
1273
+ for (let k = 0; k < magnitudes.length; k++) {
1274
+ const mag = magnitudes[k] ?? 0;
1275
+ energy += mag * mag;
1276
+ }
1277
+ return energy;
1278
+ }
1279
+ function computeFrameAmplitude(magnitudes) {
1280
+ let sum = 0;
1281
+ for (let k = 0; k < magnitudes.length; k++) {
1282
+ sum += magnitudes[k] ?? 0;
1283
+ }
1284
+ return sum;
1285
+ }
1286
+
1287
+ // src/dsp/bandMir.ts
1288
+ function computeDiagnostics(energyRetainedPerFrame) {
1289
+ const totalFrames = energyRetainedPerFrame.length;
1290
+ let sum = 0;
1291
+ let weakCount = 0;
1292
+ let emptyCount = 0;
1293
+ for (let i = 0; i < totalFrames; i++) {
1294
+ const e = energyRetainedPerFrame[i] ?? 0;
1295
+ sum += e;
1296
+ if (e < 0.01) weakCount++;
1297
+ if (e === 0) emptyCount++;
1298
+ }
1299
+ const meanEnergyRetained = totalFrames > 0 ? sum / totalFrames : 0;
1300
+ const warnings = [];
1301
+ if (meanEnergyRetained < 0.01) {
1302
+ warnings.push("Band contains very little energy - check frequency range");
1303
+ }
1304
+ if (emptyCount > totalFrames * 0.5) {
1305
+ warnings.push("More than half of frames are empty - band may not be active for this audio");
1306
+ }
1307
+ return {
1308
+ meanEnergyRetained,
1309
+ weakFrameCount: weakCount,
1310
+ emptyFrameCount: emptyCount,
1311
+ totalFrames,
1312
+ warnings
1313
+ };
1314
+ }
1315
+ function createMeta(startMs) {
1316
+ const endMs = typeof performance !== "undefined" ? performance.now() : Date.now();
1317
+ const timings = {
1318
+ totalMs: endMs - startMs,
1319
+ cpuMs: endMs - startMs,
1320
+ gpuMs: 0
1321
+ };
1322
+ return {
1323
+ backend: "cpu",
1324
+ usedGpu: false,
1325
+ timings
1326
+ };
1327
+ }
1328
+ function movingAverage(values, windowFrames) {
1329
+ if (windowFrames <= 1) return values;
1330
+ const n = values.length;
1331
+ const out = new Float32Array(n);
1332
+ const half = Math.floor(windowFrames / 2);
1333
+ const prefix = new Float64Array(n + 1);
1334
+ prefix[0] = 0;
1335
+ for (let i = 0; i < n; i++) {
1336
+ prefix[i + 1] = (prefix[i] ?? 0) + (values[i] ?? 0);
1337
+ }
1338
+ for (let i = 0; i < n; i++) {
1339
+ const start = Math.max(0, i - half);
1340
+ const end = Math.min(n, i + half + 1);
1341
+ const sum = (prefix[end] ?? 0) - (prefix[start] ?? 0);
1342
+ const count = Math.max(1, end - start);
1343
+ out[i] = sum / count;
1344
+ }
1345
+ return out;
1346
+ }
1347
+ function logCompress(x) {
1348
+ return Math.log1p(Math.max(0, x));
1349
+ }
1350
+ function bandAmplitudeEnvelope(spec, band, options) {
1351
+ const startMs = typeof performance !== "undefined" ? performance.now() : Date.now();
1352
+ const masked = applyBandMaskToSpectrogram(spec, band, {
1353
+ edgeSmoothHz: options?.edgeSmoothHz
1354
+ });
1355
+ const nFrames = masked.times.length;
1356
+ const values = new Float32Array(nFrames);
1357
+ for (let t = 0; t < nFrames; t++) {
1358
+ const mags = masked.magnitudes[t];
1359
+ if (mags) {
1360
+ values[t] = computeFrameAmplitude(mags);
1361
+ }
1362
+ }
1363
+ return {
1364
+ kind: "bandMir1d",
1365
+ bandId: band.id,
1366
+ bandLabel: band.label,
1367
+ fn: "bandAmplitudeEnvelope",
1368
+ times: masked.times,
1369
+ values,
1370
+ meta: createMeta(startMs),
1371
+ diagnostics: computeDiagnostics(masked.energyRetainedPerFrame)
1372
+ };
1373
+ }
1374
+ function bandOnsetStrength(spec, band, options) {
1375
+ const startMs = typeof performance !== "undefined" ? performance.now() : Date.now();
1376
+ const useLog = options?.onset?.useLog ?? false;
1377
+ const smoothMs = options?.onset?.smoothMs ?? 30;
1378
+ const diffMethod = options?.onset?.diffMethod ?? "rectified";
1379
+ const masked = applyBandMaskToSpectrogram(spec, band, {
1380
+ edgeSmoothHz: options?.edgeSmoothHz
1381
+ });
1382
+ const nFrames = masked.times.length;
1383
+ const nBins = (masked.fftSize >>> 1) + 1;
1384
+ const out = new Float32Array(nFrames);
1385
+ out[0] = 0;
1386
+ for (let t = 1; t < nFrames; t++) {
1387
+ const cur = masked.magnitudes[t];
1388
+ const prev = masked.magnitudes[t - 1];
1389
+ if (!cur || !prev) {
1390
+ out[t] = 0;
1391
+ continue;
1392
+ }
1393
+ let sum = 0;
1394
+ let binsWithData = 0;
1395
+ for (let k = 0; k < nBins; k++) {
1396
+ let a = cur[k] ?? 0;
1397
+ let b = prev[k] ?? 0;
1398
+ if (a > 0 || b > 0) {
1399
+ binsWithData++;
1400
+ if (useLog) {
1401
+ a = logCompress(a);
1402
+ b = logCompress(b);
1403
+ }
1404
+ const d = a - b;
1405
+ sum += diffMethod === "abs" ? Math.abs(d) : Math.max(0, d);
1406
+ }
1407
+ }
1408
+ out[t] = binsWithData > 0 ? sum / binsWithData : 0;
1409
+ }
1410
+ const smoothMs_ = smoothMs;
1411
+ if (smoothMs_ > 0 && nFrames >= 2) {
1412
+ const dt = (masked.times[1] ?? 0) - (masked.times[0] ?? 0);
1413
+ const windowFrames = Math.max(1, Math.round(smoothMs_ / 1e3 / Math.max(1e-9, dt)));
1414
+ return {
1415
+ kind: "bandMir1d",
1416
+ bandId: band.id,
1417
+ bandLabel: band.label,
1418
+ fn: "bandOnsetStrength",
1419
+ times: masked.times,
1420
+ values: movingAverage(out, windowFrames | 1),
1421
+ meta: createMeta(startMs),
1422
+ diagnostics: computeDiagnostics(masked.energyRetainedPerFrame)
1423
+ };
1424
+ }
1425
+ return {
1426
+ kind: "bandMir1d",
1427
+ bandId: band.id,
1428
+ bandLabel: band.label,
1429
+ fn: "bandOnsetStrength",
1430
+ times: masked.times,
1431
+ values: out,
1432
+ meta: createMeta(startMs),
1433
+ diagnostics: computeDiagnostics(masked.energyRetainedPerFrame)
1434
+ };
1435
+ }
1436
+ function bandSpectralFlux(spec, band, options) {
1437
+ const startMs = typeof performance !== "undefined" ? performance.now() : Date.now();
1438
+ const masked = applyBandMaskToSpectrogram(spec, band, {
1439
+ edgeSmoothHz: options?.edgeSmoothHz
1440
+ });
1441
+ const nFrames = masked.times.length;
1442
+ const nBins = (masked.fftSize >>> 1) + 1;
1443
+ const out = new Float32Array(nFrames);
1444
+ let prev = null;
1445
+ for (let t = 0; t < nFrames; t++) {
1446
+ const mags = masked.magnitudes[t];
1447
+ if (!mags) {
1448
+ out[t] = 0;
1449
+ prev = null;
1450
+ continue;
1451
+ }
1452
+ let sum = 0;
1453
+ for (let k = 0; k < nBins; k++) sum += mags[k] ?? 0;
1454
+ if (sum <= 0) {
1455
+ out[t] = 0;
1456
+ prev = null;
1457
+ continue;
1458
+ }
1459
+ const cur = new Float32Array(nBins);
1460
+ const inv = 1 / sum;
1461
+ for (let k = 0; k < nBins; k++) cur[k] = (mags[k] ?? 0) * inv;
1462
+ if (!prev) {
1463
+ out[t] = 0;
1464
+ prev = cur;
1465
+ continue;
1466
+ }
1467
+ let flux = 0;
1468
+ for (let k = 0; k < nBins; k++) {
1469
+ const d = (cur[k] ?? 0) - (prev[k] ?? 0);
1470
+ flux += Math.abs(d);
1471
+ }
1472
+ out[t] = flux;
1473
+ prev = cur;
1474
+ }
1475
+ return {
1476
+ kind: "bandMir1d",
1477
+ bandId: band.id,
1478
+ bandLabel: band.label,
1479
+ fn: "bandSpectralFlux",
1480
+ times: masked.times,
1481
+ values: out,
1482
+ meta: createMeta(startMs),
1483
+ diagnostics: computeDiagnostics(masked.energyRetainedPerFrame)
1484
+ };
1485
+ }
1486
+ function bandSpectralCentroid(spec, band, options) {
1487
+ const startMs = typeof performance !== "undefined" ? performance.now() : Date.now();
1488
+ const masked = applyBandMaskToSpectrogram(spec, band, {
1489
+ edgeSmoothHz: options?.edgeSmoothHz
1490
+ });
1491
+ const nFrames = masked.times.length;
1492
+ const nBins = (masked.fftSize >>> 1) + 1;
1493
+ const binHz = masked.sampleRate / masked.fftSize;
1494
+ const out = new Float32Array(nFrames);
1495
+ for (let t = 0; t < nFrames; t++) {
1496
+ const mags = masked.magnitudes[t];
1497
+ if (!mags) {
1498
+ out[t] = 0;
1499
+ continue;
1500
+ }
1501
+ let num = 0;
1502
+ let den = 0;
1503
+ for (let k = 0; k < nBins; k++) {
1504
+ const m = mags[k] ?? 0;
1505
+ if (m > 0) {
1506
+ const f = k * binHz;
1507
+ num += f * m;
1508
+ den += m;
1509
+ }
1510
+ }
1511
+ out[t] = den > 0 ? num / den : 0;
1512
+ }
1513
+ return {
1514
+ kind: "bandMir1d",
1515
+ bandId: band.id,
1516
+ bandLabel: band.label,
1517
+ fn: "bandSpectralCentroid",
1518
+ times: masked.times,
1519
+ values: out,
1520
+ meta: createMeta(startMs),
1521
+ diagnostics: computeDiagnostics(masked.energyRetainedPerFrame)
1522
+ };
1523
+ }
1524
+ async function runBandMirBatch(spec, request, options) {
1525
+ const startMs = typeof performance !== "undefined" ? performance.now() : Date.now();
1526
+ const results = /* @__PURE__ */ new Map();
1527
+ for (const band of request.bands) {
1528
+ if (options?.isCancelled?.()) {
1529
+ throw new Error("@octoseq/mir: cancelled");
1530
+ }
1531
+ if (!band.enabled) continue;
1532
+ const bandResults = [];
1533
+ for (const fn of request.functions) {
1534
+ if (options?.isCancelled?.()) {
1535
+ throw new Error("@octoseq/mir: cancelled");
1536
+ }
1537
+ let result;
1538
+ switch (fn) {
1539
+ case "bandAmplitudeEnvelope":
1540
+ result = bandAmplitudeEnvelope(spec, band, options);
1541
+ break;
1542
+ case "bandOnsetStrength":
1543
+ result = bandOnsetStrength(spec, band, options);
1544
+ break;
1545
+ case "bandSpectralFlux":
1546
+ result = bandSpectralFlux(spec, band, options);
1547
+ break;
1548
+ case "bandSpectralCentroid":
1549
+ result = bandSpectralCentroid(spec, band, options);
1550
+ break;
1551
+ default:
1552
+ const _exhaustive = fn;
1553
+ throw new Error(`Unknown band MIR function: ${_exhaustive}`);
1554
+ }
1555
+ bandResults.push(result);
1556
+ }
1557
+ results.set(band.id, bandResults);
1558
+ }
1559
+ const endMs = typeof performance !== "undefined" ? performance.now() : Date.now();
1560
+ return {
1561
+ results,
1562
+ totalTimingMs: endMs - startMs
1563
+ };
1564
+ }
1565
+ function getBandMirFunctionLabel(fn) {
1566
+ switch (fn) {
1567
+ case "bandAmplitudeEnvelope":
1568
+ return "Amplitude Envelope";
1569
+ case "bandOnsetStrength":
1570
+ return "Onset Strength";
1571
+ case "bandSpectralFlux":
1572
+ return "Spectral Flux";
1573
+ case "bandSpectralCentroid":
1574
+ return "Spectral Centroid";
1575
+ default:
1576
+ return fn;
1577
+ }
1578
+ }
1579
+
1580
+ // src/dsp/bandEvents.ts
1581
+ function nowMs() {
1582
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
1583
+ }
1584
+ function createMeta2(startMs) {
1585
+ const endMs = nowMs();
1586
+ const timings = {
1587
+ totalMs: endMs - startMs,
1588
+ cpuMs: endMs - startMs,
1589
+ gpuMs: 0
1590
+ };
1591
+ return {
1592
+ backend: "cpu",
1593
+ usedGpu: false,
1594
+ timings
1595
+ };
1596
+ }
1597
+ function computeEventDiagnostics(events, durationSec) {
1598
+ const eventCount2 = events.length;
1599
+ const eventsPerSecond = durationSec > 0 ? eventCount2 / durationSec : 0;
1600
+ const warnings = [];
1601
+ if (durationSec > 1 && eventCount2 === 0) {
1602
+ warnings.push("No events detected - signal may be too quiet or noisy");
1603
+ } else if (eventsPerSecond > 20) {
1604
+ warnings.push("Very high event density (>20/sec) - consider adjusting threshold");
1605
+ } else if (durationSec > 10 && eventsPerSecond < 0.1) {
1606
+ warnings.push("Very sparse events (<0.1/sec) - signal may not be active");
1607
+ }
1608
+ return {
1609
+ eventCount: eventCount2,
1610
+ eventsPerSecond,
1611
+ warnings
1612
+ };
1613
+ }
1614
+ function bandOnsetPeaks(signal, options) {
1615
+ const startMs = nowMs();
1616
+ const minIntervalSec = options?.minIntervalSec ?? 0.0625;
1617
+ const adaptiveFactor = options?.adaptiveFactor ?? 0.8;
1618
+ const strict = options?.strict ?? true;
1619
+ const { times, values } = signal;
1620
+ const pickOptions = {
1621
+ minIntervalSec,
1622
+ adaptive: {
1623
+ method: "meanStd",
1624
+ factor: adaptiveFactor
1625
+ },
1626
+ strict
1627
+ };
1628
+ const peaks = peakPick(times, values, pickOptions);
1629
+ const maxStrength = peaks.reduce((max, p) => Math.max(max, p.strength), 0);
1630
+ const events = peaks.map((p) => ({
1631
+ time: p.time,
1632
+ weight: maxStrength > 0 ? p.strength / maxStrength : 1
1633
+ }));
1634
+ const duration = times.length >= 2 ? (times[times.length - 1] ?? 0) - (times[0] ?? 0) : 0;
1635
+ return {
1636
+ kind: "bandEvents",
1637
+ bandId: signal.bandId,
1638
+ bandLabel: signal.bandLabel,
1639
+ fn: "bandOnsetPeaks",
1640
+ events,
1641
+ sourceSignal: {
1642
+ fn: signal.fn,
1643
+ times: signal.times,
1644
+ values: signal.values
1645
+ },
1646
+ meta: createMeta2(startMs),
1647
+ diagnostics: computeEventDiagnostics(events, duration)
1648
+ };
1649
+ }
1650
+ function bandBeatCandidates(onsetPeaks, options) {
1651
+ const startMs = nowMs();
1652
+ const minIntervalSec = options?.minIntervalSec ?? 0.1;
1653
+ const thresholdFactor = options?.thresholdFactor ?? 0.5;
1654
+ const weights = onsetPeaks.events.map((e) => e.weight);
1655
+ const meanWeight = weights.length > 0 ? weights.reduce((sum, w) => sum + w, 0) / weights.length : 0;
1656
+ const variance = weights.length > 0 ? weights.reduce((sum, w) => sum + (w - meanWeight) ** 2, 0) / weights.length : 0;
1657
+ const stdWeight = Math.sqrt(variance);
1658
+ const threshold = meanWeight + thresholdFactor * stdWeight;
1659
+ const candidates = [];
1660
+ let lastTime = -Infinity;
1661
+ for (const event of onsetPeaks.events) {
1662
+ if (event.weight < threshold) continue;
1663
+ if (event.time - lastTime < minIntervalSec) {
1664
+ const last = candidates[candidates.length - 1];
1665
+ if (last && event.weight > last.weight) {
1666
+ last.time = event.time;
1667
+ last.weight = event.weight;
1668
+ lastTime = event.time;
1669
+ }
1670
+ continue;
1671
+ }
1672
+ candidates.push({
1673
+ time: event.time,
1674
+ weight: event.weight,
1675
+ beatPosition: event.beatPosition,
1676
+ beatPhase: event.beatPhase
1677
+ });
1678
+ lastTime = event.time;
1679
+ }
1680
+ const duration = onsetPeaks.sourceSignal?.times ? onsetPeaks.sourceSignal.times.length >= 2 ? (onsetPeaks.sourceSignal.times[onsetPeaks.sourceSignal.times.length - 1] ?? 0) - (onsetPeaks.sourceSignal.times[0] ?? 0) : 0 : 0;
1681
+ return {
1682
+ kind: "bandEvents",
1683
+ bandId: onsetPeaks.bandId,
1684
+ bandLabel: onsetPeaks.bandLabel,
1685
+ fn: "bandBeatCandidates",
1686
+ events: candidates,
1687
+ // Don't include source signal to save memory
1688
+ meta: createMeta2(startMs),
1689
+ diagnostics: computeEventDiagnostics(candidates, duration)
1690
+ };
1691
+ }
1692
+ async function runBandEventsBatch(request) {
1693
+ const startMs = nowMs();
1694
+ const results = /* @__PURE__ */ new Map();
1695
+ const sourceFunction = request.sourceFunction ?? "bandOnsetStrength";
1696
+ for (const [bandId, mirResults] of request.bandMirResults.entries()) {
1697
+ const bandEventResults = [];
1698
+ const sourceSignal = mirResults.find((r) => r.fn === sourceFunction);
1699
+ if (!sourceSignal) {
1700
+ continue;
1701
+ }
1702
+ for (const fn of request.functions) {
1703
+ switch (fn) {
1704
+ case "bandOnsetPeaks": {
1705
+ const result = bandOnsetPeaks(sourceSignal, request.onsetPeaksOptions);
1706
+ bandEventResults.push(result);
1707
+ break;
1708
+ }
1709
+ case "bandBeatCandidates": {
1710
+ let onsetPeaksResult = bandEventResults.find(
1711
+ (r) => r.fn === "bandOnsetPeaks"
1712
+ );
1713
+ if (!onsetPeaksResult) {
1714
+ onsetPeaksResult = bandOnsetPeaks(
1715
+ sourceSignal,
1716
+ request.onsetPeaksOptions
1717
+ );
1718
+ if (!request.functions.includes("bandOnsetPeaks")) ; else {
1719
+ bandEventResults.push(onsetPeaksResult);
1720
+ }
1721
+ }
1722
+ const result = bandBeatCandidates(
1723
+ onsetPeaksResult,
1724
+ request.beatCandidatesOptions
1725
+ );
1726
+ bandEventResults.push(result);
1727
+ break;
1728
+ }
1729
+ default: {
1730
+ const _exhaustive = fn;
1731
+ throw new Error(`Unknown band event function: ${_exhaustive}`);
1732
+ }
1733
+ }
1734
+ }
1735
+ if (bandEventResults.length > 0) {
1736
+ results.set(bandId, bandEventResults);
1737
+ }
1738
+ }
1739
+ const endMs = nowMs();
1740
+ return {
1741
+ results,
1742
+ totalTimingMs: endMs - startMs
1743
+ };
1744
+ }
1745
+ function getBandEventFunctionLabel(fn) {
1746
+ switch (fn) {
1747
+ case "bandOnsetPeaks":
1748
+ return "Onset Peaks";
1749
+ case "bandBeatCandidates":
1750
+ return "Beat Candidates";
1751
+ default:
1752
+ return fn;
1753
+ }
1754
+ }
1755
+
1756
+ // src/dsp/bandCqt.ts
1757
+ function nowMs2() {
1758
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
1759
+ }
1760
+ function createMeta3(startMs) {
1761
+ const endMs = nowMs2();
1762
+ const timings = {
1763
+ totalMs: endMs - startMs,
1764
+ cpuMs: endMs - startMs,
1765
+ gpuMs: 0
1766
+ };
1767
+ return {
1768
+ backend: "cpu",
1769
+ usedGpu: false,
1770
+ timings
1771
+ };
1772
+ }
1773
+ function computeDiagnostics2(energyRetainedPerFrame) {
1774
+ const totalFrames = energyRetainedPerFrame.length;
1775
+ let sum = 0;
1776
+ let weakCount = 0;
1777
+ let emptyCount = 0;
1778
+ for (let i = 0; i < totalFrames; i++) {
1779
+ const e = energyRetainedPerFrame[i] ?? 0;
1780
+ sum += e;
1781
+ if (e < 0.01) weakCount++;
1782
+ if (e === 0) emptyCount++;
1783
+ }
1784
+ const meanEnergyRetained = totalFrames > 0 ? sum / totalFrames : 0;
1785
+ const warnings = [];
1786
+ if (meanEnergyRetained < 0.01) {
1787
+ warnings.push("Band contains very little CQT energy - check frequency range");
1788
+ }
1789
+ if (emptyCount > totalFrames * 0.5) {
1790
+ warnings.push("More than half of frames are empty in CQT - band may not be active");
1791
+ }
1792
+ return {
1793
+ meanEnergyRetained,
1794
+ weakFrameCount: weakCount,
1795
+ emptyFrameCount: emptyCount,
1796
+ totalFrames,
1797
+ warnings
1798
+ };
1799
+ }
1800
+ function getBandBoundsAtTime(band, timeSec) {
1801
+ for (const seg of band.frequencyShape) {
1802
+ if (timeSec >= seg.startTime && timeSec < seg.endTime) {
1803
+ const ratio = (timeSec - seg.startTime) / (seg.endTime - seg.startTime);
1804
+ const lowHz = seg.lowHzStart + ratio * (seg.lowHzEnd - seg.lowHzStart);
1805
+ const highHz = seg.highHzStart + ratio * (seg.highHzEnd - seg.highHzStart);
1806
+ return { lowHz, highHz };
1807
+ }
1808
+ }
1809
+ const first = band.frequencyShape[0];
1810
+ if (first) {
1811
+ return { lowHz: first.lowHzStart, highHz: first.highHzStart };
1812
+ }
1813
+ return { lowHz: 20, highHz: 2e4 };
1814
+ }
1815
+ function applyBandMaskToCqt(cqt, band, options) {
1816
+ const edgeSmoothBins = options?.edgeSmoothBins ?? 0;
1817
+ const nFrames = cqt.times.length;
1818
+ const nBins = cqt.binFrequencies.length;
1819
+ const maskedMagnitudes = new Array(nFrames);
1820
+ const energyRetainedPerFrame = new Float32Array(nFrames);
1821
+ for (let t = 0; t < nFrames; t++) {
1822
+ const frame = cqt.magnitudes[t];
1823
+ if (!frame) {
1824
+ maskedMagnitudes[t] = new Float32Array(nBins);
1825
+ energyRetainedPerFrame[t] = 0;
1826
+ continue;
1827
+ }
1828
+ const timeSec = cqt.times[t] ?? 0;
1829
+ const { lowHz, highHz } = getBandBoundsAtTime(band, timeSec);
1830
+ const lowBin = Math.max(0, Math.floor(hzToCqtBin(lowHz, cqt.config)));
1831
+ const highBin = Math.min(nBins, Math.ceil(hzToCqtBin(highHz, cqt.config)));
1832
+ const maskedFrame = new Float32Array(nBins);
1833
+ let originalEnergy = 0;
1834
+ let retainedEnergy = 0;
1835
+ for (let k = 0; k < nBins; k++) {
1836
+ const mag = frame[k] ?? 0;
1837
+ originalEnergy += mag * mag;
1838
+ if (k >= lowBin && k < highBin) {
1839
+ let weight = 1;
1840
+ if (edgeSmoothBins > 0) {
1841
+ const distFromLow = k - lowBin;
1842
+ const distFromHigh = highBin - 1 - k;
1843
+ const minDist = Math.min(distFromLow, distFromHigh);
1844
+ if (minDist < edgeSmoothBins) {
1845
+ weight = (minDist + 1) / (edgeSmoothBins + 1);
1846
+ }
1847
+ }
1848
+ maskedFrame[k] = mag * weight;
1849
+ retainedEnergy += (mag * weight) ** 2;
1850
+ }
1851
+ }
1852
+ maskedMagnitudes[t] = maskedFrame;
1853
+ energyRetainedPerFrame[t] = originalEnergy > 0 ? Math.sqrt(retainedEnergy / originalEnergy) : 0;
1854
+ }
1855
+ return {
1856
+ times: cqt.times,
1857
+ magnitudes: maskedMagnitudes,
1858
+ config: cqt.config,
1859
+ energyRetainedPerFrame,
1860
+ binFrequencies: cqt.binFrequencies
1861
+ };
1862
+ }
1863
+ function bandCqtHarmonicEnergy(cqt, band, options) {
1864
+ const startMs = nowMs2();
1865
+ const masked = applyBandMaskToCqt(cqt, band, options);
1866
+ const nFrames = masked.times.length;
1867
+ const values = new Float32Array(nFrames);
1868
+ for (let t = 0; t < nFrames; t++) {
1869
+ if (options?.isCancelled?.()) {
1870
+ throw new Error("@octoseq/mir: cancelled");
1871
+ }
1872
+ const frame = masked.magnitudes[t];
1873
+ if (!frame) {
1874
+ values[t] = 0;
1875
+ continue;
1876
+ }
1877
+ let totalEnergy = 0;
1878
+ for (let k = 0; k < frame.length; k++) {
1879
+ const mag = frame[k] ?? 0;
1880
+ totalEnergy += mag * mag;
1881
+ }
1882
+ if (totalEnergy === 0) {
1883
+ values[t] = 0;
1884
+ continue;
1885
+ }
1886
+ let maxMag = 0;
1887
+ let fundamentalBin = 0;
1888
+ for (let k = 0; k < frame.length; k++) {
1889
+ const mag = frame[k] ?? 0;
1890
+ if (mag > maxMag) {
1891
+ maxMag = mag;
1892
+ fundamentalBin = k;
1893
+ }
1894
+ }
1895
+ const fundamentalFreq = cqtBinToHz(fundamentalBin, cqt.config);
1896
+ let harmonicEnergy2 = 0;
1897
+ const numHarmonics = 6;
1898
+ for (let h = 1; h <= numHarmonics; h++) {
1899
+ const harmonicFreq = fundamentalFreq * h;
1900
+ const harmonicBin = Math.round(hzToCqtBin(harmonicFreq, cqt.config));
1901
+ if (harmonicBin >= 0 && harmonicBin < frame.length) {
1902
+ const mag = frame[harmonicBin] ?? 0;
1903
+ const weight = 1 / h;
1904
+ harmonicEnergy2 += mag * mag * weight;
1905
+ }
1906
+ }
1907
+ let weightSum = 0;
1908
+ for (let h = 1; h <= numHarmonics; h++) {
1909
+ weightSum += 1 / h;
1910
+ }
1911
+ harmonicEnergy2 /= weightSum;
1912
+ values[t] = Math.min(1, harmonicEnergy2 / totalEnergy);
1913
+ }
1914
+ const normalized = normalizeMinMax(values);
1915
+ return {
1916
+ kind: "bandCqt1d",
1917
+ bandId: band.id,
1918
+ bandLabel: band.label,
1919
+ fn: "bandCqtHarmonicEnergy",
1920
+ times: masked.times,
1921
+ values: normalized,
1922
+ meta: createMeta3(startMs),
1923
+ diagnostics: computeDiagnostics2(masked.energyRetainedPerFrame)
1924
+ };
1925
+ }
1926
+ function bandCqtBassPitchMotion(cqt, band, options) {
1927
+ const startMs = nowMs2();
1928
+ const masked = applyBandMaskToCqt(cqt, band, options);
1929
+ const nFrames = masked.times.length;
1930
+ const BASS_MIN_HZ = 20;
1931
+ const BASS_MAX_HZ = 300;
1932
+ const bassStartBin = Math.max(0, Math.floor(hzToCqtBin(BASS_MIN_HZ, cqt.config)));
1933
+ const bassEndBin = Math.min(
1934
+ masked.binFrequencies.length,
1935
+ Math.ceil(hzToCqtBin(BASS_MAX_HZ, cqt.config))
1936
+ );
1937
+ const bassNumBins = bassEndBin - bassStartBin;
1938
+ if (bassNumBins <= 0) {
1939
+ return {
1940
+ kind: "bandCqt1d",
1941
+ bandId: band.id,
1942
+ bandLabel: band.label,
1943
+ fn: "bandCqtBassPitchMotion",
1944
+ times: masked.times,
1945
+ values: new Float32Array(nFrames),
1946
+ meta: createMeta3(startMs),
1947
+ diagnostics: {
1948
+ ...computeDiagnostics2(masked.energyRetainedPerFrame),
1949
+ warnings: ["Band does not overlap bass frequency range (20-300 Hz)"]
1950
+ }
1951
+ };
1952
+ }
1953
+ const centroids = new Float32Array(nFrames);
1954
+ for (let t = 0; t < nFrames; t++) {
1955
+ const frame = masked.magnitudes[t];
1956
+ if (!frame) continue;
1957
+ let num = 0;
1958
+ let den = 0;
1959
+ for (let k = bassStartBin; k < bassEndBin; k++) {
1960
+ const mag = frame[k] ?? 0;
1961
+ if (mag > 0) {
1962
+ num += k * mag;
1963
+ den += mag;
1964
+ }
1965
+ }
1966
+ centroids[t] = den > 0 ? num / den : bassStartBin + bassNumBins / 2;
1967
+ }
1968
+ const motion = new Float32Array(nFrames);
1969
+ for (let t = 1; t < nFrames; t++) {
1970
+ motion[t] = Math.abs((centroids[t] ?? 0) - (centroids[t - 1] ?? 0));
1971
+ }
1972
+ if (nFrames > 1) {
1973
+ motion[0] = motion[1] ?? 0;
1974
+ }
1975
+ const normalized = normalizeMinMax(motion);
1976
+ return {
1977
+ kind: "bandCqt1d",
1978
+ bandId: band.id,
1979
+ bandLabel: band.label,
1980
+ fn: "bandCqtBassPitchMotion",
1981
+ times: masked.times,
1982
+ values: normalized,
1983
+ meta: createMeta3(startMs),
1984
+ diagnostics: computeDiagnostics2(masked.energyRetainedPerFrame)
1985
+ };
1986
+ }
1987
+ function bandCqtTonalStability(cqt, band, options) {
1988
+ const startMs = nowMs2();
1989
+ const masked = applyBandMaskToCqt(cqt, band, options);
1990
+ const nFrames = masked.times.length;
1991
+ const CHROMA_BINS = 12;
1992
+ const WINDOW_FRAMES = 20;
1993
+ const chromas = new Array(nFrames);
1994
+ for (let t = 0; t < nFrames; t++) {
1995
+ const frame = masked.magnitudes[t];
1996
+ const chroma = new Float32Array(CHROMA_BINS);
1997
+ if (frame) {
1998
+ const binsPerSemitone = cqt.binsPerOctave / CHROMA_BINS;
1999
+ for (let k = 0; k < frame.length; k++) {
2000
+ const chromaBin = Math.floor(k % cqt.binsPerOctave / binsPerSemitone) % CHROMA_BINS;
2001
+ const mag = frame[k] ?? 0;
2002
+ chroma[chromaBin] = (chroma[chromaBin] ?? 0) + mag * mag;
2003
+ }
2004
+ let sum = 0;
2005
+ for (let c = 0; c < CHROMA_BINS; c++) {
2006
+ sum += chroma[c] ?? 0;
2007
+ }
2008
+ if (sum > 0) {
2009
+ for (let c = 0; c < CHROMA_BINS; c++) {
2010
+ chroma[c] = (chroma[c] ?? 0) / sum;
2011
+ }
2012
+ }
2013
+ }
2014
+ chromas[t] = chroma;
2015
+ }
2016
+ const halfWindow = Math.floor(WINDOW_FRAMES / 2);
2017
+ const instability = new Float32Array(nFrames);
2018
+ for (let t = 0; t < nFrames; t++) {
2019
+ const windowStart = Math.max(0, t - halfWindow);
2020
+ const windowEnd = Math.min(nFrames, t + halfWindow + 1);
2021
+ const windowSize = windowEnd - windowStart;
2022
+ const avgChroma = new Float32Array(CHROMA_BINS);
2023
+ for (let w = windowStart; w < windowEnd; w++) {
2024
+ const chroma = chromas[w];
2025
+ if (chroma) {
2026
+ for (let c = 0; c < CHROMA_BINS; c++) {
2027
+ avgChroma[c] = (avgChroma[c] ?? 0) + (chroma[c] ?? 0);
2028
+ }
2029
+ }
2030
+ }
2031
+ for (let c = 0; c < CHROMA_BINS; c++) {
2032
+ avgChroma[c] = (avgChroma[c] ?? 0) / windowSize;
2033
+ }
2034
+ let totalVariance = 0;
2035
+ for (let w = windowStart; w < windowEnd; w++) {
2036
+ const chroma = chromas[w];
2037
+ if (chroma) {
2038
+ for (let c = 0; c < CHROMA_BINS; c++) {
2039
+ const diff = (chroma[c] ?? 0) - (avgChroma[c] ?? 0);
2040
+ totalVariance += diff * diff;
2041
+ }
2042
+ }
2043
+ }
2044
+ totalVariance /= windowSize * CHROMA_BINS;
2045
+ instability[t] = totalVariance;
2046
+ }
2047
+ const normalizedInstability = normalizeMinMax(instability);
2048
+ const stability = new Float32Array(nFrames);
2049
+ for (let t = 0; t < nFrames; t++) {
2050
+ stability[t] = 1 - (normalizedInstability[t] ?? 0);
2051
+ }
2052
+ return {
2053
+ kind: "bandCqt1d",
2054
+ bandId: band.id,
2055
+ bandLabel: band.label,
2056
+ fn: "bandCqtTonalStability",
2057
+ times: masked.times,
2058
+ values: stability,
2059
+ meta: createMeta3(startMs),
2060
+ diagnostics: computeDiagnostics2(masked.energyRetainedPerFrame)
2061
+ };
2062
+ }
2063
+ function normalizeMinMax(values) {
2064
+ const n = values.length;
2065
+ if (n === 0) return new Float32Array(0);
2066
+ let min = Infinity;
2067
+ let max = -Infinity;
2068
+ for (let i = 0; i < n; i++) {
2069
+ const v = values[i] ?? 0;
2070
+ if (v < min) min = v;
2071
+ if (v > max) max = v;
2072
+ }
2073
+ const out = new Float32Array(n);
2074
+ const range = max - min;
2075
+ if (range === 0 || !Number.isFinite(range)) {
2076
+ out.fill(0.5);
2077
+ return out;
2078
+ }
2079
+ for (let i = 0; i < n; i++) {
2080
+ out[i] = ((values[i] ?? 0) - min) / range;
2081
+ }
2082
+ return out;
2083
+ }
2084
+ async function runBandCqtBatch(cqt, request, options) {
2085
+ const startMs = nowMs2();
2086
+ const results = /* @__PURE__ */ new Map();
2087
+ for (const band of request.bands) {
2088
+ if (options?.isCancelled?.()) {
2089
+ throw new Error("@octoseq/mir: cancelled");
2090
+ }
2091
+ if (!band.enabled) continue;
2092
+ const bandResults = [];
2093
+ for (const fn of request.functions) {
2094
+ if (options?.isCancelled?.()) {
2095
+ throw new Error("@octoseq/mir: cancelled");
2096
+ }
2097
+ let result;
2098
+ switch (fn) {
2099
+ case "bandCqtHarmonicEnergy":
2100
+ result = bandCqtHarmonicEnergy(cqt, band, options);
2101
+ break;
2102
+ case "bandCqtBassPitchMotion":
2103
+ result = bandCqtBassPitchMotion(cqt, band, options);
2104
+ break;
2105
+ case "bandCqtTonalStability":
2106
+ result = bandCqtTonalStability(cqt, band, options);
2107
+ break;
2108
+ default:
2109
+ const _exhaustive = fn;
2110
+ throw new Error(`Unknown band CQT function: ${_exhaustive}`);
2111
+ }
2112
+ bandResults.push(result);
2113
+ }
2114
+ results.set(band.id, bandResults);
2115
+ }
2116
+ const endMs = nowMs2();
2117
+ return {
2118
+ results,
2119
+ totalTimingMs: endMs - startMs
2120
+ };
2121
+ }
2122
+ function getBandCqtFunctionLabel(fn) {
2123
+ switch (fn) {
2124
+ case "bandCqtHarmonicEnergy":
2125
+ return "Harmonic Energy (CQT)";
2126
+ case "bandCqtBassPitchMotion":
2127
+ return "Bass Pitch Motion (CQT)";
2128
+ case "bandCqtTonalStability":
2129
+ return "Tonal Stability (CQT)";
2130
+ default:
2131
+ return fn;
2132
+ }
2133
+ }
2134
+
2135
+ // src/dsp/bandProposal.ts
2136
+ var PROPOSAL_DEFAULTS = {
2137
+ maxProposals: 8,
2138
+ minSalience: 0.3,
2139
+ minSeparationOctaves: 0.5,
2140
+ minBandwidthHz: 20,
2141
+ analysisWindow: 0
2142
+ // 0 = full track
2143
+ };
2144
+ function generateProposalId() {
2145
+ return `proposal-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
2146
+ }
2147
+ function generateBandId2() {
2148
+ return `band-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
2149
+ }
2150
+ function computeAverageCqtSpectrum(cqt) {
2151
+ const nBins = cqt.magnitudes[0]?.length ?? 0;
2152
+ const nFrames = cqt.magnitudes.length;
2153
+ const average = new Float32Array(nBins);
2154
+ for (let frame = 0; frame < nFrames; frame++) {
2155
+ const frameMags = cqt.magnitudes[frame];
2156
+ if (!frameMags) continue;
2157
+ for (let bin = 0; bin < nBins; bin++) {
2158
+ average[bin] = (average[bin] ?? 0) + (frameMags[bin] ?? 0);
2159
+ }
2160
+ }
2161
+ for (let bin = 0; bin < nBins; bin++) {
2162
+ average[bin] = (average[bin] ?? 0) / nFrames;
2163
+ }
2164
+ return average;
2165
+ }
2166
+ function findLocalMaxima(values, minNeighborDistance = 3) {
2167
+ const peaks = [];
2168
+ for (let i = minNeighborDistance; i < values.length - minNeighborDistance; i++) {
2169
+ let isPeak = true;
2170
+ const centerVal = values[i] ?? 0;
2171
+ for (let j = -minNeighborDistance; j <= minNeighborDistance; j++) {
2172
+ if (j === 0) continue;
2173
+ if ((values[i + j] ?? 0) >= centerVal) {
2174
+ isPeak = false;
2175
+ break;
2176
+ }
2177
+ }
2178
+ if (isPeak && centerVal > 0) {
2179
+ peaks.push(i);
2180
+ }
2181
+ }
2182
+ return peaks;
2183
+ }
2184
+ function computeBandwidth(values, peakBin, cqt, minBandwidthHz) {
2185
+ const peakMag = values[peakBin] ?? 0;
2186
+ const threshold = peakMag * 0.707;
2187
+ let lowBin = peakBin;
2188
+ while (lowBin > 0 && (values[lowBin - 1] ?? 0) >= threshold) {
2189
+ lowBin--;
2190
+ }
2191
+ let highBin = peakBin;
2192
+ while (highBin < values.length - 1 && (values[highBin + 1] ?? 0) >= threshold) {
2193
+ highBin++;
2194
+ }
2195
+ if (minBandwidthHz > 0) {
2196
+ const maxExpansions = values.length;
2197
+ for (let i = 0; i < maxExpansions; i++) {
2198
+ const lowHzTmp = cqtBinToHz(lowBin, cqt.config);
2199
+ const highHzTmp = cqtBinToHz(highBin, cqt.config);
2200
+ if (highHzTmp - lowHzTmp >= minBandwidthHz) break;
2201
+ const canExpandLow = lowBin > 0;
2202
+ const canExpandHigh = highBin < values.length - 1;
2203
+ if (!canExpandLow && !canExpandHigh) break;
2204
+ if (canExpandLow && canExpandHigh) {
2205
+ lowBin--;
2206
+ highBin++;
2207
+ } else if (canExpandLow) {
2208
+ lowBin--;
2209
+ } else {
2210
+ highBin++;
2211
+ }
2212
+ }
2213
+ }
2214
+ const lowHz = cqtBinToHz(lowBin, cqt.config);
2215
+ const highHz = cqtBinToHz(highBin, cqt.config);
2216
+ const bandwidthOctaves = Math.log2(highHz / lowHz);
2217
+ return { lowBin, highBin, lowHz, highHz, bandwidthOctaves };
2218
+ }
2219
+ function computeTemporalVariance(cqt, lowBin, highBin) {
2220
+ const nFrames = cqt.magnitudes.length;
2221
+ const bandEnergies = new Float32Array(nFrames);
2222
+ for (let frame = 0; frame < nFrames; frame++) {
2223
+ const frameMags = cqt.magnitudes[frame];
2224
+ if (!frameMags) continue;
2225
+ let energy = 0;
2226
+ for (let bin = lowBin; bin <= highBin; bin++) {
2227
+ const mag = frameMags[bin] ?? 0;
2228
+ energy += mag * mag;
2229
+ }
2230
+ bandEnergies[frame] = energy;
2231
+ }
2232
+ let sum = 0;
2233
+ for (let i = 0; i < nFrames; i++) {
2234
+ sum += bandEnergies[i] ?? 0;
2235
+ }
2236
+ const mean = sum / nFrames;
2237
+ let variance = 0;
2238
+ for (let i = 0; i < nFrames; i++) {
2239
+ const diff = (bandEnergies[i] ?? 0) - mean;
2240
+ variance += diff * diff;
2241
+ }
2242
+ variance /= nFrames;
2243
+ return mean > 0 ? variance / (mean * mean) : 0;
2244
+ }
2245
+ function detectSpectralPeaks(cqt, config) {
2246
+ const avgSpectrum = computeAverageCqtSpectrum(cqt);
2247
+ const minBinDistance = Math.ceil(config.minSeparationOctaves * cqt.binsPerOctave);
2248
+ const peakIndices = findLocalMaxima(avgSpectrum, Math.max(3, minBinDistance / 2));
2249
+ const peaks = [];
2250
+ for (const binIndex of peakIndices) {
2251
+ const bw = computeBandwidth(avgSpectrum, binIndex, cqt, config.minBandwidthHz);
2252
+ peaks.push({
2253
+ binIndex,
2254
+ centerHz: cqtBinToHz(binIndex, cqt.config),
2255
+ magnitude: avgSpectrum[binIndex] ?? 0,
2256
+ bandwidth: bw.bandwidthOctaves,
2257
+ lowHz: bw.lowHz,
2258
+ highHz: bw.highHz
2259
+ });
2260
+ }
2261
+ peaks.sort((a, b) => b.magnitude - a.magnitude);
2262
+ return peaks;
2263
+ }
2264
+ function scorePeak(peak, cqt, avgSpectrum, cqtSignals) {
2265
+ const lowBin = Math.max(0, Math.floor(peak.lowHz / (cqt.config.fMin * Math.pow(2, 1 / cqt.binsPerOctave))));
2266
+ const highBin = Math.min(
2267
+ getNumBins(cqt.config) - 1,
2268
+ Math.ceil(peak.highHz / (cqt.config.fMin * Math.pow(2, 1 / cqt.binsPerOctave)))
2269
+ );
2270
+ const temporalVariance = computeTemporalVariance(cqt, lowBin, highBin);
2271
+ let bandEnergy = 0;
2272
+ let totalEnergy = 0;
2273
+ for (let bin = 0; bin < avgSpectrum.length; bin++) {
2274
+ const mag = avgSpectrum[bin] ?? 0;
2275
+ totalEnergy += mag * mag;
2276
+ if (bin >= lowBin && bin <= highBin) {
2277
+ bandEnergy += mag * mag;
2278
+ }
2279
+ }
2280
+ const energyConcentration = totalEnergy > 0 ? bandEnergy / totalEnergy : 0;
2281
+ const isBassRange = peak.centerHz < 300;
2282
+ const isLowMidRange = peak.centerHz >= 300 && peak.centerHz < 1e3;
2283
+ let avgHarmonicEnergy = 0;
2284
+ let avgBassPitchMotion = 0;
2285
+ let avgTonalStability = 0;
2286
+ const nFrames = cqtSignals.harmonicEnergy.length;
2287
+ for (let i = 0; i < nFrames; i++) {
2288
+ avgHarmonicEnergy += cqtSignals.harmonicEnergy[i] ?? 0;
2289
+ avgBassPitchMotion += cqtSignals.bassPitchMotion[i] ?? 0;
2290
+ avgTonalStability += cqtSignals.tonalStability[i] ?? 0;
2291
+ }
2292
+ avgHarmonicEnergy /= nFrames;
2293
+ avgBassPitchMotion /= nFrames;
2294
+ avgTonalStability /= nFrames;
2295
+ let source;
2296
+ let reason;
2297
+ let salience;
2298
+ if (isBassRange && avgBassPitchMotion > 0.4) {
2299
+ source = "cqt_bass_motion";
2300
+ reason = "Significant bass pitch motion detected";
2301
+ salience = 0.3 + energyConcentration * 0.3 + avgBassPitchMotion * 0.4;
2302
+ } else if (avgHarmonicEnergy > 0.5 && avgTonalStability > 0.5) {
2303
+ source = "cqt_harmonic";
2304
+ reason = "Strong harmonic structure with stable tonality";
2305
+ salience = 0.3 + avgHarmonicEnergy * 0.35 + avgTonalStability * 0.35;
2306
+ } else if (temporalVariance > 0.5) {
2307
+ source = "onset_band";
2308
+ reason = "Distinct transient activity pattern";
2309
+ salience = 0.3 + energyConcentration * 0.3 + Math.min(1, temporalVariance) * 0.4;
2310
+ } else if (energyConcentration > 0.1) {
2311
+ source = "energy_cluster";
2312
+ reason = "Concentrated spectral energy";
2313
+ salience = 0.2 + energyConcentration * 0.5 + (1 - Math.min(1, peak.bandwidth)) * 0.3;
2314
+ } else {
2315
+ source = "spectral_peak";
2316
+ reason = "Persistent spectral peak";
2317
+ salience = 0.2 + energyConcentration * 0.4 + (1 - Math.min(1, peak.bandwidth * 2)) * 0.4;
2318
+ }
2319
+ if (isBassRange) {
2320
+ reason += " (bass region)";
2321
+ } else if (isLowMidRange) {
2322
+ reason += " (low-mid region)";
2323
+ } else if (peak.centerHz > 4e3) {
2324
+ reason += " (high frequency region)";
2325
+ }
2326
+ return {
2327
+ peak,
2328
+ salience: Math.min(1, Math.max(0, salience)),
2329
+ source,
2330
+ reason
2331
+ };
2332
+ }
2333
+ function createBandFromCandidate(candidate, duration) {
2334
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2335
+ const provenance = {
2336
+ source: "manual",
2337
+ // Will be treated as imported when promoted
2338
+ createdAt: now
2339
+ };
2340
+ return {
2341
+ id: generateBandId2(),
2342
+ label: `Region ${Math.round(candidate.peak.centerHz)} Hz`,
2343
+ sourceId: "mixdown",
2344
+ // Proposals default to mixdown; user assigns sourceId on promotion
2345
+ enabled: true,
2346
+ timeScope: { kind: "global" },
2347
+ frequencyShape: [
2348
+ {
2349
+ startTime: 0,
2350
+ endTime: duration,
2351
+ lowHzStart: candidate.peak.lowHz,
2352
+ highHzStart: candidate.peak.highHz,
2353
+ lowHzEnd: candidate.peak.lowHz,
2354
+ highHzEnd: candidate.peak.highHz
2355
+ }
2356
+ ],
2357
+ sortOrder: 0,
2358
+ provenance
2359
+ };
2360
+ }
2361
+ function filterOverlappingCandidates(candidates, minSeparationOctaves) {
2362
+ const filtered = [];
2363
+ for (const candidate of candidates) {
2364
+ let hasOverlap = false;
2365
+ for (const existing of filtered) {
2366
+ const octaveDiff = Math.abs(
2367
+ Math.log2(candidate.peak.centerHz / existing.peak.centerHz)
2368
+ );
2369
+ if (octaveDiff < minSeparationOctaves) {
2370
+ hasOverlap = true;
2371
+ break;
2372
+ }
2373
+ }
2374
+ if (!hasOverlap) {
2375
+ filtered.push(candidate);
2376
+ }
2377
+ }
2378
+ return filtered;
2379
+ }
2380
+ async function generateBandProposals(audio, duration, options = {}) {
2381
+ const startTime = performance.now();
2382
+ const config = {
2383
+ ...PROPOSAL_DEFAULTS,
2384
+ ...options.config
2385
+ };
2386
+ const cqtConfig = withCqtDefaults({
2387
+ binsPerOctave: 24,
2388
+ fMin: 32.7,
2389
+ fMax: Math.min(8372, audio.sampleRate / 2)
2390
+ // Cap at Nyquist
2391
+ });
2392
+ const cqt = await cqtSpectrogram(audio, cqtConfig, {
2393
+ isCancelled: options.isCancelled
2394
+ });
2395
+ if (options.isCancelled?.()) {
2396
+ throw new Error("@octoseq/mir: cancelled");
2397
+ }
2398
+ const harmonicResult = harmonicEnergy(cqt);
2399
+ const bassMotionResult = bassPitchMotion(cqt);
2400
+ const tonalResult = tonalStability(cqt);
2401
+ const cqtSignals = {
2402
+ harmonicEnergy: harmonicResult.values,
2403
+ bassPitchMotion: bassMotionResult.values,
2404
+ tonalStability: tonalResult.values
2405
+ };
2406
+ const peaks = detectSpectralPeaks(cqt, config);
2407
+ const avgSpectrum = computeAverageCqtSpectrum(cqt);
2408
+ const candidates = [];
2409
+ for (const peak of peaks) {
2410
+ if (options.isCancelled?.()) {
2411
+ throw new Error("@octoseq/mir: cancelled");
2412
+ }
2413
+ const candidate = scorePeak(peak, cqt, avgSpectrum, cqtSignals);
2414
+ if (candidate.salience >= config.minSalience) {
2415
+ candidates.push(candidate);
2416
+ }
2417
+ }
2418
+ candidates.sort((a, b) => b.salience - a.salience);
2419
+ const filtered = filterOverlappingCandidates(candidates, config.minSeparationOctaves);
2420
+ const finalCandidates = filtered.slice(0, config.maxProposals);
2421
+ const proposals = finalCandidates.map((candidate, index) => {
2422
+ const band = createBandFromCandidate(candidate, duration);
2423
+ band.sortOrder = index;
2424
+ return {
2425
+ id: generateProposalId(),
2426
+ band,
2427
+ salience: candidate.salience,
2428
+ reason: candidate.reason,
2429
+ source: candidate.source,
2430
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
2431
+ };
2432
+ });
2433
+ const endTime = performance.now();
2434
+ const meta = {
2435
+ backend: "cpu",
2436
+ usedGpu: false,
2437
+ timings: {
2438
+ totalMs: endTime - startTime,
2439
+ cpuMs: endTime - startTime
2440
+ }
2441
+ };
2442
+ return {
2443
+ kind: "bandProposals",
2444
+ proposals,
2445
+ meta
2446
+ };
2447
+ }
2448
+
2449
+ // src/dsp/customSignalReduction.ts
2450
+ function logCompress2(x) {
2451
+ return Math.log1p(Math.max(0, x));
2452
+ }
2453
+ function movingAverage2(values, windowFrames) {
2454
+ if (windowFrames <= 1) return values;
2455
+ const n = values.length;
2456
+ const out = new Float32Array(n);
2457
+ const half = Math.floor(windowFrames / 2);
2458
+ const prefix = new Float64Array(n + 1);
2459
+ prefix[0] = 0;
2460
+ for (let i = 0; i < n; i++) {
2461
+ prefix[i + 1] = (prefix[i] ?? 0) + (values[i] ?? 0);
2462
+ }
2463
+ for (let i = 0; i < n; i++) {
2464
+ const start = Math.max(0, i - half);
2465
+ const end = Math.min(n, i + half + 1);
2466
+ const sum = (prefix[end] ?? 0) - (prefix[start] ?? 0);
2467
+ const count = Math.max(1, end - start);
2468
+ out[i] = sum / count;
2469
+ }
2470
+ return out;
2471
+ }
2472
+ function computeValueRange(values) {
2473
+ if (values.length === 0) {
2474
+ return { min: 0, max: 0 };
2475
+ }
2476
+ let min = values[0] ?? 0;
2477
+ let max = values[0] ?? 0;
2478
+ for (let i = 1; i < values.length; i++) {
2479
+ const v = values[i] ?? 0;
2480
+ if (v < min) min = v;
2481
+ if (v > max) max = v;
2482
+ }
2483
+ return { min, max };
2484
+ }
2485
+ function getBinRange(numBins, options) {
2486
+ const low = Math.max(0, options?.lowBin ?? 0);
2487
+ const high = Math.min(numBins, options?.highBin ?? numBins);
2488
+ return { low, high };
2489
+ }
2490
+ function reduceMean(input, options) {
2491
+ const nFrames = input.data.length;
2492
+ const values = new Float32Array(nFrames);
2493
+ for (let t = 0; t < nFrames; t++) {
2494
+ const frame = input.data[t];
2495
+ if (!frame || frame.length === 0) {
2496
+ values[t] = 0;
2497
+ continue;
2498
+ }
2499
+ const { low, high } = getBinRange(frame.length, options?.binRange);
2500
+ let sum = 0;
2501
+ const count = high - low;
2502
+ for (let k = low; k < high; k++) {
2503
+ sum += frame[k] ?? 0;
2504
+ }
2505
+ values[t] = count > 0 ? sum / count : 0;
2506
+ }
2507
+ return {
2508
+ times: input.times,
2509
+ values,
2510
+ valueRange: computeValueRange(values)
2511
+ };
2512
+ }
2513
+ function reduceMax(input, options) {
2514
+ const nFrames = input.data.length;
2515
+ const values = new Float32Array(nFrames);
2516
+ for (let t = 0; t < nFrames; t++) {
2517
+ const frame = input.data[t];
2518
+ if (!frame || frame.length === 0) {
2519
+ values[t] = 0;
2520
+ continue;
2521
+ }
2522
+ const { low, high } = getBinRange(frame.length, options?.binRange);
2523
+ let max = -Infinity;
2524
+ for (let k = low; k < high; k++) {
2525
+ const v = frame[k] ?? 0;
2526
+ if (v > max) max = v;
2527
+ }
2528
+ values[t] = max === -Infinity ? 0 : max;
11
2529
  }
12
- static async create() {
13
- if (typeof navigator === "undefined") {
14
- throw new Error(
15
- "@octoseq/mir: WebGPU is only available in the browser (navigator is undefined)."
16
- );
2530
+ return {
2531
+ times: input.times,
2532
+ values,
2533
+ valueRange: computeValueRange(values)
2534
+ };
2535
+ }
2536
+ function reduceSum(input, options) {
2537
+ const nFrames = input.data.length;
2538
+ const values = new Float32Array(nFrames);
2539
+ for (let t = 0; t < nFrames; t++) {
2540
+ const frame = input.data[t];
2541
+ if (!frame || frame.length === 0) {
2542
+ values[t] = 0;
2543
+ continue;
17
2544
  }
18
- const nav = navigator;
19
- if (!nav.gpu) {
20
- throw new Error(
21
- "@octoseq/mir: WebGPU is unavailable (navigator.gpu is missing). Use CPU mode or a WebGPU-capable browser."
22
- );
2545
+ const { low, high } = getBinRange(frame.length, options?.binRange);
2546
+ let sum = 0;
2547
+ for (let k = low; k < high; k++) {
2548
+ sum += frame[k] ?? 0;
23
2549
  }
24
- const adapter = await nav.gpu.requestAdapter();
25
- if (!adapter) {
26
- throw new Error(
27
- "@octoseq/mir: Failed to acquire a WebGPU adapter. WebGPU may be disabled or unsupported."
28
- );
2550
+ values[t] = sum;
2551
+ }
2552
+ return {
2553
+ times: input.times,
2554
+ values,
2555
+ valueRange: computeValueRange(values)
2556
+ };
2557
+ }
2558
+ function reduceVariance(input, options) {
2559
+ const nFrames = input.data.length;
2560
+ const values = new Float32Array(nFrames);
2561
+ for (let t = 0; t < nFrames; t++) {
2562
+ const frame = input.data[t];
2563
+ if (!frame || frame.length === 0) {
2564
+ values[t] = 0;
2565
+ continue;
29
2566
  }
30
- const device = await adapter.requestDevice();
31
- return new _MirGPU(device);
2567
+ const { low, high } = getBinRange(frame.length, options?.binRange);
2568
+ const count = high - low;
2569
+ if (count <= 1) {
2570
+ values[t] = 0;
2571
+ continue;
2572
+ }
2573
+ let sum = 0;
2574
+ for (let k = low; k < high; k++) {
2575
+ sum += frame[k] ?? 0;
2576
+ }
2577
+ const mean = sum / count;
2578
+ let variance = 0;
2579
+ for (let k = low; k < high; k++) {
2580
+ const d = (frame[k] ?? 0) - mean;
2581
+ variance += d * d;
2582
+ }
2583
+ values[t] = variance / count;
32
2584
  }
33
- };
2585
+ return {
2586
+ times: input.times,
2587
+ values,
2588
+ valueRange: computeValueRange(values)
2589
+ };
2590
+ }
2591
+ function reduceAmplitude(input, options) {
2592
+ return reduceSum(input, options);
2593
+ }
2594
+ function reduceSpectralFlux(input, options) {
2595
+ const nFrames = input.data.length;
2596
+ const values = new Float32Array(nFrames);
2597
+ const normalized = options?.spectralFlux?.normalized ?? true;
2598
+ if (nFrames === 0) {
2599
+ return {
2600
+ times: input.times,
2601
+ values,
2602
+ valueRange: { min: 0, max: 0 }
2603
+ };
2604
+ }
2605
+ values[0] = 0;
2606
+ let prevNorm = null;
2607
+ for (let t = 0; t < nFrames; t++) {
2608
+ const frame = input.data[t];
2609
+ if (!frame || frame.length === 0) {
2610
+ values[t] = 0;
2611
+ prevNorm = null;
2612
+ continue;
2613
+ }
2614
+ const { low, high } = getBinRange(frame.length, options?.binRange);
2615
+ let curNorm;
2616
+ if (normalized) {
2617
+ let sum = 0;
2618
+ for (let k = low; k < high; k++) {
2619
+ sum += frame[k] ?? 0;
2620
+ }
2621
+ if (sum <= 0) {
2622
+ values[t] = 0;
2623
+ prevNorm = null;
2624
+ continue;
2625
+ }
2626
+ const inv = 1 / sum;
2627
+ curNorm = new Float32Array(high - low);
2628
+ for (let k = low; k < high; k++) {
2629
+ curNorm[k - low] = (frame[k] ?? 0) * inv;
2630
+ }
2631
+ } else {
2632
+ curNorm = new Float32Array(high - low);
2633
+ for (let k = low; k < high; k++) {
2634
+ curNorm[k - low] = frame[k] ?? 0;
2635
+ }
2636
+ }
2637
+ if (!prevNorm || prevNorm.length !== curNorm.length) {
2638
+ values[t] = 0;
2639
+ prevNorm = curNorm;
2640
+ continue;
2641
+ }
2642
+ let flux = 0;
2643
+ for (let k = 0; k < curNorm.length; k++) {
2644
+ flux += Math.abs((curNorm[k] ?? 0) - (prevNorm[k] ?? 0));
2645
+ }
2646
+ values[t] = flux;
2647
+ prevNorm = curNorm;
2648
+ }
2649
+ return {
2650
+ times: input.times,
2651
+ values,
2652
+ valueRange: computeValueRange(values)
2653
+ };
2654
+ }
2655
+ function reduceSpectralCentroid(input, options) {
2656
+ const nFrames = input.data.length;
2657
+ const values = new Float32Array(nFrames);
2658
+ for (let t = 0; t < nFrames; t++) {
2659
+ const frame = input.data[t];
2660
+ if (!frame || frame.length === 0) {
2661
+ values[t] = 0;
2662
+ continue;
2663
+ }
2664
+ const { low, high } = getBinRange(frame.length, options?.binRange);
2665
+ let num = 0;
2666
+ let den = 0;
2667
+ for (let k = low; k < high; k++) {
2668
+ const m = frame[k] ?? 0;
2669
+ if (m > 0) {
2670
+ num += k * m;
2671
+ den += m;
2672
+ }
2673
+ }
2674
+ values[t] = den > 0 ? num / den : 0;
2675
+ }
2676
+ return {
2677
+ times: input.times,
2678
+ values,
2679
+ valueRange: computeValueRange(values)
2680
+ };
2681
+ }
2682
+ function reduceOnsetStrength(input, options) {
2683
+ const nFrames = input.data.length;
2684
+ const values = new Float32Array(nFrames);
2685
+ const useLog = options?.onsetStrength?.useLog ?? true;
2686
+ const smoothMs = options?.onsetStrength?.smoothMs ?? 10;
2687
+ const diffMethod = options?.onsetStrength?.diffMethod ?? "rectified";
2688
+ if (nFrames === 0) {
2689
+ return {
2690
+ times: input.times,
2691
+ values,
2692
+ valueRange: { min: 0, max: 0 }
2693
+ };
2694
+ }
2695
+ values[0] = 0;
2696
+ for (let t = 1; t < nFrames; t++) {
2697
+ const cur = input.data[t];
2698
+ const prev = input.data[t - 1];
2699
+ if (!cur || !prev || cur.length === 0 || prev.length === 0) {
2700
+ values[t] = 0;
2701
+ continue;
2702
+ }
2703
+ const { low, high } = getBinRange(cur.length, options?.binRange);
2704
+ let sum = 0;
2705
+ let binsWithData = 0;
2706
+ for (let k = low; k < high; k++) {
2707
+ let a = cur[k] ?? 0;
2708
+ let b = prev[k] ?? 0;
2709
+ if (a > 0 || b > 0) {
2710
+ binsWithData++;
2711
+ if (useLog) {
2712
+ a = logCompress2(a);
2713
+ b = logCompress2(b);
2714
+ }
2715
+ const d = a - b;
2716
+ sum += diffMethod === "abs" ? Math.abs(d) : Math.max(0, d);
2717
+ }
2718
+ }
2719
+ values[t] = binsWithData > 0 ? sum / binsWithData : 0;
2720
+ }
2721
+ if (smoothMs > 0 && nFrames >= 2) {
2722
+ const dt = (input.times[1] ?? 0) - (input.times[0] ?? 0);
2723
+ if (dt > 0) {
2724
+ const windowFrames = Math.max(1, Math.round(smoothMs / 1e3 / dt));
2725
+ const smoothed = movingAverage2(values, windowFrames | 1);
2726
+ return {
2727
+ times: input.times,
2728
+ values: smoothed,
2729
+ valueRange: computeValueRange(smoothed)
2730
+ };
2731
+ }
2732
+ }
2733
+ return {
2734
+ times: input.times,
2735
+ values,
2736
+ valueRange: computeValueRange(values)
2737
+ };
2738
+ }
2739
+ function reduce2DToSignal(input, algorithm, options) {
2740
+ switch (algorithm) {
2741
+ case "mean":
2742
+ return reduceMean(input, options);
2743
+ case "max":
2744
+ return reduceMax(input, options);
2745
+ case "sum":
2746
+ return reduceSum(input, options);
2747
+ case "variance":
2748
+ return reduceVariance(input, options);
2749
+ case "amplitude":
2750
+ return reduceAmplitude(input, options);
2751
+ case "spectralFlux":
2752
+ return reduceSpectralFlux(input, options);
2753
+ case "spectralCentroid":
2754
+ return reduceSpectralCentroid(input, options);
2755
+ case "onsetStrength":
2756
+ return reduceOnsetStrength(input, options);
2757
+ default:
2758
+ const _exhaustive = algorithm;
2759
+ throw new Error(`Unknown reduction algorithm: ${_exhaustive}`);
2760
+ }
2761
+ }
2762
+ function getReductionAlgorithmLabel(algorithm) {
2763
+ switch (algorithm) {
2764
+ case "mean":
2765
+ return "Mean";
2766
+ case "max":
2767
+ return "Maximum";
2768
+ case "sum":
2769
+ return "Sum";
2770
+ case "variance":
2771
+ return "Variance";
2772
+ case "amplitude":
2773
+ return "Amplitude Envelope";
2774
+ case "spectralFlux":
2775
+ return "Spectral Flux";
2776
+ case "spectralCentroid":
2777
+ return "Spectral Centroid";
2778
+ case "onsetStrength":
2779
+ return "Onset Strength";
2780
+ default:
2781
+ return String(algorithm);
2782
+ }
2783
+ }
2784
+ function getReductionAlgorithmDescription(algorithm) {
2785
+ switch (algorithm) {
2786
+ case "mean":
2787
+ return "Average value across all bins per frame";
2788
+ case "max":
2789
+ return "Maximum value across all bins per frame";
2790
+ case "sum":
2791
+ return "Sum of all bin values per frame";
2792
+ case "variance":
2793
+ return "Variance of bin values per frame";
2794
+ case "amplitude":
2795
+ return "Sum of magnitudes (energy envelope)";
2796
+ case "spectralFlux":
2797
+ return "Change between consecutive frames";
2798
+ case "spectralCentroid":
2799
+ return "Weighted center frequency";
2800
+ case "onsetStrength":
2801
+ return "Temporal derivative for onset detection";
2802
+ default:
2803
+ return "";
2804
+ }
2805
+ }
2806
+ function applyPolarity(values, mode) {
2807
+ if (mode === "signed") {
2808
+ return values;
2809
+ }
2810
+ const result = new Float32Array(values.length);
2811
+ for (let i = 0; i < values.length; i++) {
2812
+ result[i] = Math.abs(values[i] ?? 0);
2813
+ }
2814
+ return result;
2815
+ }
2816
+ function getStabilizationWindowFrames(mode, frameTime) {
2817
+ const smoothingTimes = {
2818
+ none: 0,
2819
+ light: 0.01,
2820
+ // 10ms
2821
+ medium: 0.03,
2822
+ // 30ms
2823
+ heavy: 0.1
2824
+ // 100ms
2825
+ };
2826
+ const smoothMs = smoothingTimes[mode] * 1e3;
2827
+ if (smoothMs <= 0 || frameTime <= 0) return 1;
2828
+ return Math.max(1, Math.round(smoothMs / 1e3 / frameTime)) | 1;
2829
+ }
2830
+ function applyAttackRelease(values, times, attackTimeSec, releaseTimeSec) {
2831
+ const n = values.length;
2832
+ if (n === 0) return values;
2833
+ const out = new Float32Array(n);
2834
+ out[0] = values[0] ?? 0;
2835
+ for (let i = 1; i < n; i++) {
2836
+ const dt = (times[i] ?? 0) - (times[i - 1] ?? 0);
2837
+ if (dt <= 0) {
2838
+ out[i] = values[i] ?? 0;
2839
+ continue;
2840
+ }
2841
+ const current = values[i] ?? 0;
2842
+ const prev = out[i - 1] ?? 0;
2843
+ if (current > prev) {
2844
+ if (attackTimeSec > 0) {
2845
+ const alpha = 1 - Math.exp(-dt / attackTimeSec);
2846
+ out[i] = prev + alpha * (current - prev);
2847
+ } else {
2848
+ out[i] = current;
2849
+ }
2850
+ } else {
2851
+ if (releaseTimeSec > 0) {
2852
+ const alpha = 1 - Math.exp(-dt / releaseTimeSec);
2853
+ out[i] = prev + alpha * (current - prev);
2854
+ } else {
2855
+ out[i] = current;
2856
+ }
2857
+ }
2858
+ }
2859
+ return out;
2860
+ }
2861
+ function stabilizeSignal(values, times, options) {
2862
+ if (values.length === 0) return values;
2863
+ let result = values;
2864
+ if (options.mode !== "none" && times.length >= 2) {
2865
+ const dt = (times[1] ?? 0) - (times[0] ?? 0);
2866
+ if (dt > 0) {
2867
+ const windowFrames = getStabilizationWindowFrames(options.mode, dt);
2868
+ if (windowFrames > 1) {
2869
+ result = movingAverage2(result, windowFrames);
2870
+ }
2871
+ }
2872
+ }
2873
+ if (options.envelopeMode === "attackRelease") {
2874
+ const attackSec = options.attackTimeSec ?? 0.01;
2875
+ const releaseSec = options.releaseTimeSec ?? 0.1;
2876
+ result = applyAttackRelease(result, times, attackSec, releaseSec);
2877
+ }
2878
+ return result;
2879
+ }
2880
+ function computePercentiles(values, percentiles) {
2881
+ if (values.length === 0) {
2882
+ return Object.fromEntries(percentiles.map((p) => [p, 0]));
2883
+ }
2884
+ const sorted = Float32Array.from(values).sort((a, b) => a - b);
2885
+ const n = sorted.length;
2886
+ const result = {};
2887
+ for (const p of percentiles) {
2888
+ const clamped = Math.max(0, Math.min(100, p));
2889
+ const index = clamped / 100 * (n - 1);
2890
+ const lower = Math.floor(index);
2891
+ const upper = Math.min(lower + 1, n - 1);
2892
+ const frac = index - lower;
2893
+ result[p] = (sorted[lower] ?? 0) * (1 - frac) + (sorted[upper] ?? 0) * frac;
2894
+ }
2895
+ return result;
2896
+ }
2897
+ function computeLocalStats(values, times, startTime, endTime) {
2898
+ const indices = [];
2899
+ for (let i = 0; i < times.length; i++) {
2900
+ const t = times[i] ?? 0;
2901
+ if (t >= startTime && t <= endTime) {
2902
+ indices.push(i);
2903
+ }
2904
+ }
2905
+ if (indices.length === 0) {
2906
+ return { min: 0, max: 0, p5: 0, p95: 0 };
2907
+ }
2908
+ const viewportValues = new Float32Array(indices.length);
2909
+ for (let i = 0; i < indices.length; i++) {
2910
+ viewportValues[i] = values[indices[i] ?? 0] ?? 0;
2911
+ }
2912
+ const range = computeValueRange(viewportValues);
2913
+ const percentiles = computePercentiles(viewportValues, [5, 95]);
2914
+ return {
2915
+ min: range.min,
2916
+ max: range.max,
2917
+ p5: percentiles[5] ?? 0,
2918
+ p95: percentiles[95] ?? 0
2919
+ };
2920
+ }
2921
+
2922
+ // src/dsp/eventToSignal.ts
2923
+ function generateGaussianEnvelope(centerSample, widthSamples, numSamples) {
2924
+ const envelope = new Float32Array(numSamples);
2925
+ const sigma = widthSamples / 4;
2926
+ const twoSigmaSq = 2 * sigma * sigma;
2927
+ const startSample = Math.max(0, Math.floor(centerSample - widthSamples));
2928
+ const endSample = Math.min(numSamples, Math.ceil(centerSample + widthSamples));
2929
+ for (let i = startSample; i < endSample; i++) {
2930
+ const diff = i - centerSample;
2931
+ envelope[i] = Math.exp(-(diff * diff) / twoSigmaSq);
2932
+ }
2933
+ return envelope;
2934
+ }
2935
+ function generateAttackDecayEnvelope(startSample, attackSamples, decaySamples, numSamples) {
2936
+ const envelope = new Float32Array(numSamples);
2937
+ const attackEnd = Math.min(numSamples, startSample + attackSamples);
2938
+ for (let i = startSample; i < attackEnd; i++) {
2939
+ if (i >= 0) {
2940
+ const t = (i - startSample) / attackSamples;
2941
+ envelope[i] = t;
2942
+ }
2943
+ }
2944
+ const decayStart = startSample + attackSamples;
2945
+ const decayEnd = Math.min(numSamples, decayStart + decaySamples);
2946
+ for (let i = decayStart; i < decayEnd; i++) {
2947
+ if (i >= 0) {
2948
+ const t = (i - decayStart) / decaySamples;
2949
+ envelope[i] = 1 - t;
2950
+ }
2951
+ }
2952
+ return envelope;
2953
+ }
2954
+ function generateGateEnvelope(startSample, durationSamples, numSamples) {
2955
+ const envelope = new Float32Array(numSamples);
2956
+ const endSample = Math.min(numSamples, startSample + durationSamples);
2957
+ for (let i = Math.max(0, startSample); i < endSample; i++) {
2958
+ envelope[i] = 1;
2959
+ }
2960
+ return envelope;
2961
+ }
2962
+ function eventCount(events, windowSpec, options) {
2963
+ const { sampleRate, duration, normalize = false } = options;
2964
+ const numSamples = Math.ceil(duration * sampleRate);
2965
+ const values = new Float32Array(numSamples);
2966
+ const times = new Float32Array(numSamples);
2967
+ for (let i = 0; i < numSamples; i++) {
2968
+ times[i] = i / sampleRate;
2969
+ }
2970
+ const windowSamples = windowSpec.kind === "seconds" ? Math.ceil(windowSpec.windowSize * sampleRate) : windowSpec.windowSize;
2971
+ const halfWindow = Math.floor(windowSamples / 2);
2972
+ const sortedEvents = [...events].sort((a, b) => a.time - b.time);
2973
+ for (let i = 0; i < numSamples; i++) {
2974
+ const windowStart = (i - halfWindow) / sampleRate;
2975
+ const windowEnd = (i + halfWindow) / sampleRate;
2976
+ let count = 0;
2977
+ for (const event of sortedEvents) {
2978
+ if (event.time >= windowStart && event.time < windowEnd) {
2979
+ count++;
2980
+ }
2981
+ if (event.time >= windowEnd) break;
2982
+ }
2983
+ values[i] = count;
2984
+ }
2985
+ let min = 0;
2986
+ let max = 0;
2987
+ if (values.length > 0) {
2988
+ min = values[0];
2989
+ max = values[0];
2990
+ for (let i = 1; i < values.length; i++) {
2991
+ const v = values[i];
2992
+ if (v < min) min = v;
2993
+ if (v > max) max = v;
2994
+ }
2995
+ }
2996
+ if (normalize && max > min) {
2997
+ const range = max - min;
2998
+ for (let i = 0; i < values.length; i++) {
2999
+ values[i] = (values[i] - min) / range;
3000
+ }
3001
+ }
3002
+ return { values, times, rawRange: { min, max } };
3003
+ }
3004
+ function eventDensity(events, windowSpec, options) {
3005
+ const result = eventCount(events, windowSpec, { ...options, normalize: false });
3006
+ const windowSeconds = windowSpec.kind === "seconds" ? windowSpec.windowSize : windowSpec.windowSize / options.sampleRate;
3007
+ for (let i = 0; i < result.values.length; i++) {
3008
+ result.values[i] = result.values[i] / windowSeconds;
3009
+ }
3010
+ let min = 0;
3011
+ let max = 0;
3012
+ if (result.values.length > 0) {
3013
+ min = result.values[0];
3014
+ max = result.values[0];
3015
+ for (let i = 1; i < result.values.length; i++) {
3016
+ const v = result.values[i];
3017
+ if (v < min) min = v;
3018
+ if (v > max) max = v;
3019
+ }
3020
+ }
3021
+ if (options.normalize && max > min) {
3022
+ const range = max - min;
3023
+ for (let i = 0; i < result.values.length; i++) {
3024
+ result.values[i] = (result.values[i] - min) / range;
3025
+ }
3026
+ }
3027
+ return { ...result, rawRange: { min, max } };
3028
+ }
3029
+ function weightedSum(events, windowSpec, options) {
3030
+ const { sampleRate, duration, normalize = false } = options;
3031
+ const numSamples = Math.ceil(duration * sampleRate);
3032
+ const values = new Float32Array(numSamples);
3033
+ const times = new Float32Array(numSamples);
3034
+ for (let i = 0; i < numSamples; i++) {
3035
+ times[i] = i / sampleRate;
3036
+ }
3037
+ const windowSamples = windowSpec.kind === "seconds" ? Math.ceil(windowSpec.windowSize * sampleRate) : windowSpec.windowSize;
3038
+ const halfWindow = Math.floor(windowSamples / 2);
3039
+ const sortedEvents = [...events].sort((a, b) => a.time - b.time);
3040
+ for (let i = 0; i < numSamples; i++) {
3041
+ const windowStart = (i - halfWindow) / sampleRate;
3042
+ const windowEnd = (i + halfWindow) / sampleRate;
3043
+ let sum = 0;
3044
+ for (const event of sortedEvents) {
3045
+ if (event.time >= windowStart && event.time < windowEnd) {
3046
+ sum += event.weight ?? 1;
3047
+ }
3048
+ if (event.time >= windowEnd) break;
3049
+ }
3050
+ values[i] = sum;
3051
+ }
3052
+ let min = 0;
3053
+ let max = 0;
3054
+ if (values.length > 0) {
3055
+ min = values[0];
3056
+ max = values[0];
3057
+ for (let i = 1; i < values.length; i++) {
3058
+ const v = values[i];
3059
+ if (v < min) min = v;
3060
+ if (v > max) max = v;
3061
+ }
3062
+ }
3063
+ if (normalize && max > min) {
3064
+ const range = max - min;
3065
+ for (let i = 0; i < values.length; i++) {
3066
+ values[i] = (values[i] - min) / range;
3067
+ }
3068
+ }
3069
+ return { values, times, rawRange: { min, max } };
3070
+ }
3071
+ function weightedMean(events, windowSpec, options) {
3072
+ const { sampleRate, duration, normalize = false } = options;
3073
+ const numSamples = Math.ceil(duration * sampleRate);
3074
+ const values = new Float32Array(numSamples);
3075
+ const times = new Float32Array(numSamples);
3076
+ for (let i = 0; i < numSamples; i++) {
3077
+ times[i] = i / sampleRate;
3078
+ }
3079
+ const windowSamples = windowSpec.kind === "seconds" ? Math.ceil(windowSpec.windowSize * sampleRate) : windowSpec.windowSize;
3080
+ const halfWindow = Math.floor(windowSamples / 2);
3081
+ const sortedEvents = [...events].sort((a, b) => a.time - b.time);
3082
+ for (let i = 0; i < numSamples; i++) {
3083
+ const windowStart = (i - halfWindow) / sampleRate;
3084
+ const windowEnd = (i + halfWindow) / sampleRate;
3085
+ let sum = 0;
3086
+ let count = 0;
3087
+ for (const event of sortedEvents) {
3088
+ if (event.time >= windowStart && event.time < windowEnd) {
3089
+ sum += event.weight ?? 1;
3090
+ count++;
3091
+ }
3092
+ if (event.time >= windowEnd) break;
3093
+ }
3094
+ values[i] = count > 0 ? sum / count : 0;
3095
+ }
3096
+ let min = 0;
3097
+ let max = 0;
3098
+ if (values.length > 0) {
3099
+ min = values[0];
3100
+ max = values[0];
3101
+ for (let i = 1; i < values.length; i++) {
3102
+ const v = values[i];
3103
+ if (v < min) min = v;
3104
+ if (v > max) max = v;
3105
+ }
3106
+ }
3107
+ if (normalize && max > min) {
3108
+ const range = max - min;
3109
+ for (let i = 0; i < values.length; i++) {
3110
+ values[i] = (values[i] - min) / range;
3111
+ }
3112
+ }
3113
+ return { values, times, rawRange: { min, max } };
3114
+ }
3115
+ function eventEnvelope(events, shape, options) {
3116
+ const { sampleRate, duration, normalize = false } = options;
3117
+ const numSamples = Math.ceil(duration * sampleRate);
3118
+ const values = new Float32Array(numSamples);
3119
+ const times = new Float32Array(numSamples);
3120
+ for (let i = 0; i < numSamples; i++) {
3121
+ times[i] = i / sampleRate;
3122
+ }
3123
+ for (const event of events) {
3124
+ const eventSample = Math.floor(event.time * sampleRate);
3125
+ const weight = event.weight ?? 1;
3126
+ let envelope;
3127
+ switch (shape.kind) {
3128
+ case "impulse":
3129
+ if (eventSample >= 0 && eventSample < numSamples) {
3130
+ values[eventSample] = values[eventSample] + weight;
3131
+ }
3132
+ continue;
3133
+ case "gaussian":
3134
+ const widthSamples = shape.widthMs / 1e3 * sampleRate;
3135
+ envelope = generateGaussianEnvelope(eventSample, widthSamples, numSamples);
3136
+ break;
3137
+ case "attackDecay":
3138
+ const attackSamples = shape.attackMs / 1e3 * sampleRate;
3139
+ const decaySamples = shape.decayMs / 1e3 * sampleRate;
3140
+ envelope = generateAttackDecayEnvelope(eventSample, attackSamples, decaySamples, numSamples);
3141
+ break;
3142
+ case "gate":
3143
+ const durationSamples = event.duration ? event.duration * sampleRate : sampleRate * 0.1;
3144
+ envelope = generateGateEnvelope(eventSample, durationSamples, numSamples);
3145
+ break;
3146
+ default:
3147
+ continue;
3148
+ }
3149
+ for (let i = 0; i < numSamples; i++) {
3150
+ values[i] = values[i] + envelope[i] * weight;
3151
+ }
3152
+ }
3153
+ let min = 0;
3154
+ let max = 0;
3155
+ if (values.length > 0) {
3156
+ min = values[0];
3157
+ max = values[0];
3158
+ for (let i = 1; i < values.length; i++) {
3159
+ const v = values[i];
3160
+ if (v < min) min = v;
3161
+ if (v > max) max = v;
3162
+ }
3163
+ }
3164
+ if (normalize && max > min) {
3165
+ const range = max - min;
3166
+ for (let i = 0; i < values.length; i++) {
3167
+ values[i] = (values[i] - min) / range;
3168
+ }
3169
+ }
3170
+ return { values, times, rawRange: { min, max } };
3171
+ }
3172
+ function eventsToSignal(events, params, options) {
3173
+ const defaultWindow = { kind: "seconds", windowSize: 0.5 };
3174
+ const defaultShape = { kind: "attackDecay", attackMs: 5, decayMs: 100 };
3175
+ switch (params.reducer) {
3176
+ case "eventCount":
3177
+ return eventCount(events, params.window ?? defaultWindow, options);
3178
+ case "eventDensity":
3179
+ return eventDensity(events, params.window ?? defaultWindow, options);
3180
+ case "weightedSum":
3181
+ return weightedSum(events, params.window ?? defaultWindow, options);
3182
+ case "weightedMean":
3183
+ return weightedMean(events, params.window ?? defaultWindow, options);
3184
+ case "envelope":
3185
+ return eventEnvelope(events, params.envelopeShape ?? defaultShape, options);
3186
+ default:
3187
+ throw new Error(`Unknown event reducer: ${params.reducer}`);
3188
+ }
3189
+ }
3190
+
3191
+ // src/dsp/signalTransforms.ts
3192
+ function smoothMovingAverage(values, windowMs, sampleRate) {
3193
+ const windowSamples = Math.max(1, Math.round(windowMs / 1e3 * sampleRate)) | 1;
3194
+ if (windowSamples <= 1) return values;
3195
+ const n = values.length;
3196
+ const result = new Float32Array(n);
3197
+ const halfWindow = Math.floor(windowSamples / 2);
3198
+ for (let i = 0; i < n; i++) {
3199
+ const start = Math.max(0, i - halfWindow);
3200
+ const end = Math.min(n, i + halfWindow + 1);
3201
+ let sum = 0;
3202
+ for (let j = start; j < end; j++) {
3203
+ sum += values[j];
3204
+ }
3205
+ result[i] = sum / (end - start);
3206
+ }
3207
+ return result;
3208
+ }
3209
+ function smoothExponential(values, timeConstantMs, sampleRate) {
3210
+ const n = values.length;
3211
+ if (n === 0) return values;
3212
+ const result = new Float32Array(n);
3213
+ const dt = 1 / sampleRate;
3214
+ const alpha = 1 - Math.exp(-dt / (timeConstantMs / 1e3));
3215
+ result[0] = values[0];
3216
+ for (let i = 1; i < n; i++) {
3217
+ result[i] = result[i - 1] + alpha * (values[i] - result[i - 1]);
3218
+ }
3219
+ return result;
3220
+ }
3221
+ function smoothGaussian(values, windowMs, sampleRate) {
3222
+ const windowSamples = Math.max(1, Math.round(windowMs / 1e3 * sampleRate));
3223
+ if (windowSamples <= 1) return values;
3224
+ const n = values.length;
3225
+ const result = new Float32Array(n);
3226
+ const sigma = windowSamples / 4;
3227
+ const halfWindow = Math.floor(windowSamples / 2);
3228
+ const kernel = new Float32Array(windowSamples);
3229
+ let kernelSum = 0;
3230
+ for (let i = 0; i < windowSamples; i++) {
3231
+ const x = i - halfWindow;
3232
+ kernel[i] = Math.exp(-(x * x) / (2 * sigma * sigma));
3233
+ kernelSum += kernel[i];
3234
+ }
3235
+ for (let i = 0; i < windowSamples; i++) {
3236
+ kernel[i] = kernel[i] / kernelSum;
3237
+ }
3238
+ for (let i = 0; i < n; i++) {
3239
+ let sum = 0;
3240
+ let weightSum = 0;
3241
+ for (let j = 0; j < windowSamples; j++) {
3242
+ const idx = i - halfWindow + j;
3243
+ if (idx >= 0 && idx < n) {
3244
+ sum += values[idx] * kernel[j];
3245
+ weightSum += kernel[j];
3246
+ }
3247
+ }
3248
+ result[i] = weightSum > 0 ? sum / weightSum : values[i];
3249
+ }
3250
+ return result;
3251
+ }
3252
+ function normalizeMinMax2(values, targetMin = 0, targetMax = 1) {
3253
+ const n = values.length;
3254
+ if (n === 0) return values;
3255
+ let min = values[0];
3256
+ let max = values[0];
3257
+ for (let i = 1; i < n; i++) {
3258
+ const v = values[i];
3259
+ if (v < min) min = v;
3260
+ if (v > max) max = v;
3261
+ }
3262
+ if (max === min) {
3263
+ const result2 = new Float32Array(n);
3264
+ result2.fill((targetMin + targetMax) / 2);
3265
+ return result2;
3266
+ }
3267
+ const result = new Float32Array(n);
3268
+ const sourceRange = max - min;
3269
+ const targetRange = targetMax - targetMin;
3270
+ for (let i = 0; i < n; i++) {
3271
+ result[i] = targetMin + (values[i] - min) / sourceRange * targetRange;
3272
+ }
3273
+ return result;
3274
+ }
3275
+ function normalizeRobust(values, percentileLow = 5, percentileHigh = 95, targetMin = 0, targetMax = 1) {
3276
+ const n = values.length;
3277
+ if (n === 0) return values;
3278
+ const sorted = Array.from(values).sort((a, b) => a - b);
3279
+ const lowIdx = Math.floor(percentileLow / 100 * (n - 1));
3280
+ const highIdx = Math.floor(percentileHigh / 100 * (n - 1));
3281
+ const pLow = sorted[lowIdx];
3282
+ const pHigh = sorted[highIdx];
3283
+ if (pHigh === pLow) {
3284
+ const result2 = new Float32Array(n);
3285
+ result2.fill((targetMin + targetMax) / 2);
3286
+ return result2;
3287
+ }
3288
+ const result = new Float32Array(n);
3289
+ const sourceRange = pHigh - pLow;
3290
+ const targetRange = targetMax - targetMin;
3291
+ for (let i = 0; i < n; i++) {
3292
+ const normalized = (values[i] - pLow) / sourceRange;
3293
+ const clamped = Math.max(0, Math.min(1, normalized));
3294
+ result[i] = targetMin + clamped * targetRange;
3295
+ }
3296
+ return result;
3297
+ }
3298
+ function normalizeZScore(values) {
3299
+ const n = values.length;
3300
+ if (n === 0) return values;
3301
+ let sum = 0;
3302
+ for (let i = 0; i < n; i++) {
3303
+ sum += values[i];
3304
+ }
3305
+ const mean = sum / n;
3306
+ let variance = 0;
3307
+ for (let i = 0; i < n; i++) {
3308
+ const diff = values[i] - mean;
3309
+ variance += diff * diff;
3310
+ }
3311
+ const stdDev = Math.sqrt(variance / n);
3312
+ if (stdDev === 0) {
3313
+ const result2 = new Float32Array(n);
3314
+ result2.fill(0);
3315
+ return result2;
3316
+ }
3317
+ const result = new Float32Array(n);
3318
+ for (let i = 0; i < n; i++) {
3319
+ result[i] = (values[i] - mean) / stdDev;
3320
+ }
3321
+ return result;
3322
+ }
3323
+ function applyScale(values, scale, offset) {
3324
+ const n = values.length;
3325
+ const result = new Float32Array(n);
3326
+ for (let i = 0; i < n; i++) {
3327
+ result[i] = values[i] * scale + offset;
3328
+ }
3329
+ return result;
3330
+ }
3331
+ function applyPolarityTransform(values, mode) {
3332
+ if (mode === "signed") {
3333
+ return values;
3334
+ }
3335
+ const n = values.length;
3336
+ const result = new Float32Array(n);
3337
+ for (let i = 0; i < n; i++) {
3338
+ result[i] = Math.abs(values[i]);
3339
+ }
3340
+ return result;
3341
+ }
3342
+ function applyClamp(values, min, max) {
3343
+ const n = values.length;
3344
+ const result = new Float32Array(n);
3345
+ for (let i = 0; i < n; i++) {
3346
+ let v = values[i];
3347
+ if (min !== void 0 && v < min) v = min;
3348
+ if (max !== void 0 && v > max) v = max;
3349
+ result[i] = v;
3350
+ }
3351
+ return result;
3352
+ }
3353
+ function applyRemap(values, inputMin, inputMax, outputMin, outputMax, curve = "linear") {
3354
+ const n = values.length;
3355
+ const result = new Float32Array(n);
3356
+ const inputRange = inputMax - inputMin;
3357
+ const outputRange = outputMax - outputMin;
3358
+ if (inputRange === 0) {
3359
+ result.fill((outputMin + outputMax) / 2);
3360
+ return result;
3361
+ }
3362
+ for (let i = 0; i < n; i++) {
3363
+ let t = (values[i] - inputMin) / inputRange;
3364
+ t = Math.max(0, Math.min(1, t));
3365
+ switch (curve) {
3366
+ case "easeIn":
3367
+ t = t * t;
3368
+ break;
3369
+ case "easeOut":
3370
+ t = 1 - (1 - t) * (1 - t);
3371
+ break;
3372
+ case "ease":
3373
+ t = t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
3374
+ break;
3375
+ }
3376
+ result[i] = outputMin + t * outputRange;
3377
+ }
3378
+ return result;
3379
+ }
3380
+ function applyTransformStep(values, step, context) {
3381
+ switch (step.kind) {
3382
+ case "smooth":
3383
+ switch (step.method) {
3384
+ case "movingAverage":
3385
+ return smoothMovingAverage(values, step.windowMs, context.sampleRate);
3386
+ case "exponential":
3387
+ return smoothExponential(values, step.timeConstantMs, context.sampleRate);
3388
+ case "gaussian":
3389
+ return smoothGaussian(values, step.windowMs, context.sampleRate);
3390
+ default:
3391
+ return values;
3392
+ }
3393
+ case "normalize":
3394
+ switch (step.method) {
3395
+ case "minMax":
3396
+ return normalizeMinMax2(values, step.targetMin, step.targetMax);
3397
+ case "robust":
3398
+ return normalizeRobust(
3399
+ values,
3400
+ step.percentileLow,
3401
+ step.percentileHigh,
3402
+ step.targetMin,
3403
+ step.targetMax
3404
+ );
3405
+ case "zScore":
3406
+ return normalizeZScore(values);
3407
+ default:
3408
+ return values;
3409
+ }
3410
+ case "scale":
3411
+ return applyScale(values, step.scale, step.offset);
3412
+ case "polarity":
3413
+ return applyPolarityTransform(values, step.mode);
3414
+ case "clamp":
3415
+ return applyClamp(values, step.min, step.max);
3416
+ case "remap":
3417
+ return applyRemap(
3418
+ values,
3419
+ step.inputMin,
3420
+ step.inputMax,
3421
+ step.outputMin,
3422
+ step.outputMax,
3423
+ step.curve
3424
+ );
3425
+ default:
3426
+ return values;
3427
+ }
3428
+ }
3429
+ function applyTransformChain(values, chain, context) {
3430
+ let result = values;
3431
+ for (const step of chain) {
3432
+ result = applyTransformStep(result, step, context);
3433
+ }
3434
+ return result;
3435
+ }
3436
+ function getTransformLabel(step) {
3437
+ switch (step.kind) {
3438
+ case "smooth":
3439
+ switch (step.method) {
3440
+ case "movingAverage":
3441
+ return `Smooth (${step.windowMs}ms avg)`;
3442
+ case "exponential":
3443
+ return `Smooth (${step.timeConstantMs}ms exp)`;
3444
+ case "gaussian":
3445
+ return `Smooth (${step.windowMs}ms gauss)`;
3446
+ default:
3447
+ return "Smooth";
3448
+ }
3449
+ case "normalize":
3450
+ switch (step.method) {
3451
+ case "minMax":
3452
+ return `Normalize (${step.targetMin ?? 0}-${step.targetMax ?? 1})`;
3453
+ case "robust":
3454
+ return `Normalize (robust ${step.percentileLow ?? 5}-${step.percentileHigh ?? 95}%)`;
3455
+ case "zScore":
3456
+ return "Normalize (z-score)";
3457
+ default:
3458
+ return "Normalize";
3459
+ }
3460
+ case "scale":
3461
+ return step.offset !== 0 ? `Scale (\xD7${step.scale} + ${step.offset})` : `Scale (\xD7${step.scale})`;
3462
+ case "polarity":
3463
+ return step.mode === "magnitude" ? "Magnitude" : "Signed";
3464
+ case "clamp":
3465
+ if (step.min !== void 0 && step.max !== void 0) {
3466
+ return `Clamp (${step.min}-${step.max})`;
3467
+ } else if (step.min !== void 0) {
3468
+ return `Clamp (\u2265${step.min})`;
3469
+ } else if (step.max !== void 0) {
3470
+ return `Clamp (\u2264${step.max})`;
3471
+ }
3472
+ return "Clamp";
3473
+ case "remap":
3474
+ return `Remap (${step.inputMin}-${step.inputMax} \u2192 ${step.outputMin}-${step.outputMax})`;
3475
+ default:
3476
+ return "Unknown";
3477
+ }
3478
+ }
3479
+ function createDefaultTransform(kind) {
3480
+ switch (kind) {
3481
+ case "smooth":
3482
+ return { kind: "smooth", method: "movingAverage", windowMs: 10 };
3483
+ case "normalize":
3484
+ return { kind: "normalize", method: "minMax", targetMin: 0, targetMax: 1 };
3485
+ case "scale":
3486
+ return { kind: "scale", scale: 1, offset: 0 };
3487
+ case "polarity":
3488
+ return { kind: "polarity", mode: "signed" };
3489
+ case "clamp":
3490
+ return { kind: "clamp", min: 0, max: 1 };
3491
+ case "remap":
3492
+ return { kind: "remap", inputMin: 0, inputMax: 1, outputMin: 0, outputMax: 1 };
3493
+ default:
3494
+ return null;
3495
+ }
3496
+ }
34
3497
 
35
3498
  // src/util/normalise.ts
36
3499
  function normaliseForWaveform(data, options = {}) {
@@ -96,6 +3559,28 @@ function clampDb(db2d, minDb, maxDb) {
96
3559
  return out;
97
3560
  }
98
3561
 
3562
+ // src/dsp/resample.ts
3563
+ function resample(samples, fromRate, toRate) {
3564
+ if (fromRate === toRate) {
3565
+ return samples;
3566
+ }
3567
+ const ratio = fromRate / toRate;
3568
+ const newLength = Math.floor(samples.length / ratio);
3569
+ if (newLength <= 0) {
3570
+ return new Float32Array(0);
3571
+ }
3572
+ const result = new Float32Array(newLength);
3573
+ for (let i = 0; i < newLength; i++) {
3574
+ const srcIndex = i * ratio;
3575
+ const srcIndexFloor = Math.floor(srcIndex);
3576
+ const frac = srcIndex - srcIndexFloor;
3577
+ const a = samples[srcIndexFloor] ?? 0;
3578
+ const b = samples[srcIndexFloor + 1] ?? a;
3579
+ result[i] = a + frac * (b - a);
3580
+ }
3581
+ return result;
3582
+ }
3583
+
99
3584
  // src/util/stats.ts
100
3585
  function minMax(values) {
101
3586
  const n = values.length >>> 0;
@@ -296,7 +3781,7 @@ function similarityFingerprintV1(a, b, weights = {}) {
296
3781
  }
297
3782
 
298
3783
  // src/search/searchTrackV1.ts
299
- function nowMs() {
3784
+ function nowMs3() {
300
3785
  return typeof performance !== "undefined" ? performance.now() : Date.now();
301
3786
  }
302
3787
  function clamp01(x) {
@@ -305,7 +3790,7 @@ function clamp01(x) {
305
3790
  return x;
306
3791
  }
307
3792
  async function searchTrackV1(params) {
308
- const tStart = nowMs();
3793
+ const tStart = nowMs3();
309
3794
  const options = params.options ?? {};
310
3795
  const hopSec = Math.max(5e-3, options.hopSec ?? 0.03);
311
3796
  const threshold = clamp01(options.threshold ?? 0.75);
@@ -313,7 +3798,7 @@ async function searchTrackV1(params) {
313
3798
  const qt1 = Math.max(params.queryRegion.t0, params.queryRegion.t1);
314
3799
  const windowSec = Math.max(1e-3, qt1 - qt0);
315
3800
  const minSpacingSec = Math.max(0, options.minCandidateSpacingSec ?? windowSec * 0.8);
316
- const tFp0 = nowMs();
3801
+ const tFp0 = nowMs3();
317
3802
  const queryFp = fingerprintV1({
318
3803
  t0: qt0,
319
3804
  t1: qt1,
@@ -322,8 +3807,8 @@ async function searchTrackV1(params) {
322
3807
  mfcc: params.mfcc,
323
3808
  peakPick: options.queryPeakPick
324
3809
  });
325
- const fingerprintMs = nowMs() - tFp0;
326
- const scanStartMs = nowMs();
3810
+ const fingerprintMs = nowMs3() - tFp0;
3811
+ const scanStartMs = nowMs3();
327
3812
  const trackDuration = Math.max(
328
3813
  params.mel.times.length ? params.mel.times[params.mel.times.length - 1] ?? 0 : 0,
329
3814
  params.onsetEnvelope.times.length ? params.onsetEnvelope.times[params.onsetEnvelope.times.length - 1] ?? 0 : 0
@@ -363,7 +3848,7 @@ async function searchTrackV1(params) {
363
3848
  sim[w] = clamp01(score);
364
3849
  scannedWindows++;
365
3850
  }
366
- const scanMs = nowMs() - scanStartMs;
3851
+ const scanMs = nowMs3() - scanStartMs;
367
3852
  const events = peakPick(times, sim, {
368
3853
  threshold,
369
3854
  minIntervalSec: minSpacingSec,
@@ -379,7 +3864,7 @@ async function searchTrackV1(params) {
379
3864
  windowEndSec
380
3865
  };
381
3866
  });
382
- const totalMs = nowMs() - tStart;
3867
+ const totalMs = nowMs3() - tStart;
383
3868
  return {
384
3869
  times,
385
3870
  similarity: sim,
@@ -613,7 +4098,7 @@ function scoreWithModelV1(model, x) {
613
4098
  }
614
4099
 
615
4100
  // src/search/searchTrackV1Guided.ts
616
- function nowMs2() {
4101
+ function nowMs4() {
617
4102
  return typeof performance !== "undefined" ? performance.now() : Date.now();
618
4103
  }
619
4104
  function clamp013(x) {
@@ -780,7 +4265,7 @@ var SlidingMoments = class {
780
4265
  }
781
4266
  };
782
4267
  async function searchTrackV1Guided(params) {
783
- const tStart = nowMs2();
4268
+ const tStart = nowMs4();
784
4269
  const options = params.options ?? {};
785
4270
  const hopSec = Math.max(5e-3, options.hopSec ?? 0.03);
786
4271
  const threshold = clamp013(options.threshold ?? 0.75);
@@ -799,7 +4284,7 @@ async function searchTrackV1Guided(params) {
799
4284
  positives: modelDecision.positives.length,
800
4285
  negatives: modelDecision.negatives.length
801
4286
  } : { kind: "baseline", positives: 0, negatives: 0 };
802
- const tPrep0 = nowMs2();
4287
+ const tPrep0 = nowMs4();
803
4288
  const timesFrames = params.mel.times;
804
4289
  const nFrames = timesFrames.length;
805
4290
  const trackDuration = Math.max(
@@ -810,7 +4295,7 @@ async function searchTrackV1Guided(params) {
810
4295
  const times = new Float32Array(nWindows);
811
4296
  const scores = new Float32Array(nWindows);
812
4297
  if (nWindows === 0) {
813
- const totalMs2 = nowMs2() - tStart;
4298
+ const totalMs2 = nowMs4() - tStart;
814
4299
  return {
815
4300
  times,
816
4301
  scores,
@@ -899,7 +4384,7 @@ async function searchTrackV1Guided(params) {
899
4384
  }
900
4385
  const peakPrefix = new Uint32Array(nFrames + 1);
901
4386
  for (let i = 0; i < nFrames; i++) peakPrefix[i + 1] = (peakPrefix[i] ?? 0) + (isPeak[i] ?? 0);
902
- const fingerprintMs = nowMs2() - tPrep0;
4387
+ const fingerprintMs = nowMs4() - tPrep0;
903
4388
  const addMelFrame = (frame, sum, sumSq) => {
904
4389
  const row = melBands[frame];
905
4390
  const s = melScale[frame] ?? 1;
@@ -1085,7 +4570,7 @@ async function searchTrackV1Guided(params) {
1085
4570
  };
1086
4571
  let skippedWindows = 0;
1087
4572
  let scannedWindows = 0;
1088
- const scanStartMs = nowMs2();
4573
+ const scanStartMs = nowMs4();
1089
4574
  let curveKind = "similarity";
1090
4575
  let modelExplain = baselineExplain;
1091
4576
  let modelMs = 0;
@@ -1108,7 +4593,7 @@ async function searchTrackV1Guided(params) {
1108
4593
  if (modelDecision.kind === "baseline") {
1109
4594
  runBaselineSimilarityScan();
1110
4595
  } else {
1111
- const tModel0 = nowMs2();
4596
+ const tModel0 = nowMs4();
1112
4597
  curveKind = "confidence";
1113
4598
  try {
1114
4599
  const dim = layout.dim;
@@ -1173,10 +4658,10 @@ async function searchTrackV1Guided(params) {
1173
4658
  zInvStd = null;
1174
4659
  runBaselineSimilarityScan();
1175
4660
  } finally {
1176
- modelMs = nowMs2() - tModel0;
4661
+ modelMs = nowMs4() - tModel0;
1177
4662
  }
1178
4663
  }
1179
- const scanMs = nowMs2() - scanStartMs;
4664
+ const scanMs = nowMs4() - scanStartMs;
1180
4665
  const events = peakPick(times, scores, {
1181
4666
  threshold,
1182
4667
  minIntervalSec: minSpacingSec,
@@ -1203,7 +4688,7 @@ async function searchTrackV1Guided(params) {
1203
4688
  };
1204
4689
  }
1205
4690
  }
1206
- const totalMs = nowMs2() - tStart;
4691
+ const totalMs = nowMs4() - tStart;
1207
4692
  return {
1208
4693
  times,
1209
4694
  scores,
@@ -1229,6 +4714,6 @@ function helloMir(name = "world") {
1229
4714
  return `Hello, ${name} from @octoseq/mir v${MIR_VERSION}`;
1230
4715
  }
1231
4716
 
1232
- export { MIR_VERSION, MirGPU, clampDb, fingerprintToVectorV1, fingerprintV1, helloMir, minMax, normaliseForWaveform, searchTrackV1, searchTrackV1Guided, similarityFingerprintV1, spectrogramToDb };
4717
+ export { DEFAULT_PEAK_PICKING_PARAMS, MIR_VERSION, MirGPU, addBandToStructure, allFrequencyBoundsAt, applyBandMaskToCqt, applyBandMaskToSpectrogram, applyHysteresisGate, applyPolarity, applyTransformChain, applyTransformStep, bandAmplitudeEnvelope, bandBeatCandidates, bandCqtBassPitchMotion, bandCqtHarmonicEnergy, bandCqtTonalStability, bandOnsetPeaks, bandOnsetStrength, bandSpectralCentroid, bandSpectralFlux, bandsActiveAt, binToHz, clampDb, computeAdaptiveThreshold, computeBandMaskAtTime, computeBeatPosition, computeBeatPositionFromStructure, computeFrameAmplitude, computeFrameEnergy, computeLocalStats, computePercentiles, computePhaseHypotheses, createBandStructure, createConstantBand, createDefaultTransform, createMusicalTimeStructure, createSectionedBand, createSegmentFromGrid, createStandardBands, eventCount, eventDensity, eventEnvelope, eventsToSignal, findBandById, findSegmentAtTime, fingerprintToVectorV1, fingerprintV1, frequencyBoundsAt, generateBandId, generateBandProposals, generateBeatTimes, generateSegmentBeatTimes, generateSegmentId, getBandCqtFunctionLabel, getBandEventFunctionLabel, getBandMirFunctionLabel, getReductionAlgorithmDescription, getReductionAlgorithmLabel, getTransformLabel, helloMir, hzToBin, keyframesFromBand, mergeAdjacentSegments, minMax, moveKeyframeTime, normaliseForWaveform, pickPeaks, pickPeaksAdaptive, reduce2DToSignal, removeBandFromStructure, removeKeyframe, resample, runBandCqtBatch, runBandEventsBatch, runBandMirBatch, searchTrackV1, searchTrackV1Guided, segmentsFromKeyframes, similarityFingerprintV1, sortBands, sortFrequencySegments, sortSegments, spectrogramToDb, splitBandSegmentAt, splitSegment, stabilizeSignal, touchStructure, updateBandInStructure, updateKeyframe, validateBandStructure, validateFrequencyBand, validateFrequencySegments, validateSegments, weightedMean, weightedSum };
1233
4718
  //# sourceMappingURL=index.js.map
1234
4719
  //# sourceMappingURL=index.js.map