@octoseq/mir 0.1.0-main.994cb4e → 0.1.0-main.9ea6d2e

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { withCqtDefaults, cqtSpectrogram, harmonicEnergy, bassPitchMotion, tonalStability, peakPick, cqtBinToHz, getNumBins } from './chunk-OLIDGECY.js';
2
- export { CQT_DEFAULTS, bassPitchMotion, beatSalienceFromMel, computeAllCqtSignals, computeCqt, computeCqtSignal, cqtBinToHz, cqtSpectrogram, delta, deltaDelta, detectBeatCandidates, featureIndexToHz, generateTempoHypotheses, getCqtBinFrequencies, getNumBins, getNumOctaves, harmonicEnergy, hpss, hzToCqtBin, hzToFeatureIndex, hzToMel, melSpectrogram, melToHz, mfcc, onsetEnvelopeFromMel, onsetEnvelopeFromMelGpu, onsetEnvelopeFromSpectrogram, peakPick, runMir, spectralCentroid, spectralFlux, spectrogram, tonalStability, withCqtDefaults } from './chunk-OLIDGECY.js';
1
+ import { peakPick, hzToCqtBin, cqtBinToHz, withCqtDefaults, cqtSpectrogram, harmonicEnergy, bassPitchMotion, tonalStability, getNumBins } from './chunk-CI7QGWP7.js';
2
+ export { CQT_DEFAULTS, amplitudeEnvelope, bassPitchMotion, beatSalienceFromMel, computeAllCqtSignals, computeCqt, computeCqtSignal, cqtBinToHz, cqtSpectrogram, delta, deltaDelta, detectBeatCandidates, featureIndexToHz, generateTempoHypotheses, getCqtBinFrequencies, getNumBins, getNumOctaves, harmonicEnergy, hpss, hzToCqtBin, hzToFeatureIndex, hzToMel, melSpectrogram, melToHz, mfcc, onsetEnvelopeFromMel, onsetEnvelopeFromMelGpu, onsetEnvelopeFromSpectrogram, peakPick, runMir, spectralCentroid, spectralFlux, spectrogram, tonalStability, withCqtDefaults } from './chunk-CI7QGWP7.js';
3
3
 
4
4
  // src/gpu/context.ts
5
5
  var MirGPU = class _MirGPU {
@@ -104,6 +104,314 @@ function generateBeatTimes(bpm, phaseOffset, userNudge, audioDuration) {
104
104
  return beats;
105
105
  }
106
106
 
107
+ // src/dsp/peakPicking.ts
108
+ var DEFAULT_PEAK_PICKING_PARAMS = {
109
+ threshold: 0.3,
110
+ minDistance: 0.1,
111
+ preMax: 2,
112
+ postMax: 2
113
+ };
114
+ function pickPeaks(times, values, params = {}) {
115
+ const {
116
+ threshold,
117
+ minDistance,
118
+ preMax,
119
+ postMax
120
+ } = { ...DEFAULT_PEAK_PICKING_PARAMS, ...params };
121
+ if (times.length === 0 || values.length === 0) {
122
+ return {
123
+ times: new Float32Array(0),
124
+ strengths: new Float32Array(0)
125
+ };
126
+ }
127
+ let minVal = values[0];
128
+ let maxVal = values[0];
129
+ for (let i = 1; i < values.length; i++) {
130
+ const v = values[i];
131
+ if (v < minVal) minVal = v;
132
+ if (v > maxVal) maxVal = v;
133
+ }
134
+ const range = maxVal - minVal;
135
+ if (range <= 0) {
136
+ return {
137
+ times: new Float32Array(0),
138
+ strengths: new Float32Array(0)
139
+ };
140
+ }
141
+ const normalize = (v) => (v - minVal) / range;
142
+ const peakIndices = [];
143
+ const peakStrengths = [];
144
+ for (let i = preMax; i < values.length - postMax; i++) {
145
+ const currentVal = values[i];
146
+ const normalizedVal = normalize(currentVal);
147
+ if (normalizedVal < threshold) continue;
148
+ let isMax = true;
149
+ for (let j = 1; j <= preMax; j++) {
150
+ if (values[i - j] >= currentVal) {
151
+ isMax = false;
152
+ break;
153
+ }
154
+ }
155
+ if (isMax) {
156
+ for (let j = 1; j <= postMax; j++) {
157
+ if (values[i + j] > currentVal) {
158
+ isMax = false;
159
+ break;
160
+ }
161
+ }
162
+ }
163
+ if (isMax) {
164
+ peakIndices.push(i);
165
+ peakStrengths.push(normalizedVal);
166
+ }
167
+ }
168
+ const filteredIndices = [];
169
+ const filteredStrengths = [];
170
+ for (let i = 0; i < peakIndices.length; i++) {
171
+ const idx = peakIndices[i];
172
+ const time = times[idx];
173
+ const strength = peakStrengths[i];
174
+ let shouldKeep = true;
175
+ for (let j = 0; j < filteredIndices.length; j++) {
176
+ const prevIdx = filteredIndices[j];
177
+ const prevTime = times[prevIdx];
178
+ const prevStrength = filteredStrengths[j];
179
+ if (Math.abs(time - prevTime) < minDistance) {
180
+ if (strength > prevStrength) {
181
+ filteredIndices[j] = idx;
182
+ filteredStrengths[j] = strength;
183
+ }
184
+ shouldKeep = false;
185
+ break;
186
+ }
187
+ }
188
+ if (shouldKeep) {
189
+ filteredIndices.push(idx);
190
+ filteredStrengths.push(strength);
191
+ }
192
+ }
193
+ const resultTimes = new Float32Array(filteredIndices.length);
194
+ const resultStrengths = new Float32Array(filteredStrengths.length);
195
+ for (let i = 0; i < filteredIndices.length; i++) {
196
+ resultTimes[i] = times[filteredIndices[i]];
197
+ resultStrengths[i] = filteredStrengths[i];
198
+ }
199
+ return {
200
+ times: resultTimes,
201
+ strengths: resultStrengths
202
+ };
203
+ }
204
+ function computeAdaptiveThreshold(values, windowSize = 20, thresholdMultiplier = 1.5) {
205
+ if (values.length === 0) {
206
+ return new Float32Array(0);
207
+ }
208
+ let minVal = values[0];
209
+ let maxVal = values[0];
210
+ for (let i = 1; i < values.length; i++) {
211
+ const v = values[i];
212
+ if (v < minVal) minVal = v;
213
+ if (v > maxVal) maxVal = v;
214
+ }
215
+ const range = maxVal - minVal;
216
+ if (range <= 0) {
217
+ return new Float32Array(values.length).fill(0.5);
218
+ }
219
+ const halfWindow = Math.floor(windowSize / 2);
220
+ const thresholdCurve = new Float32Array(values.length);
221
+ for (let i = 0; i < values.length; i++) {
222
+ const start = Math.max(0, i - halfWindow);
223
+ const end = Math.min(values.length, i + halfWindow + 1);
224
+ const windowLen = end - start;
225
+ let sum = 0;
226
+ for (let j = start; j < end; j++) {
227
+ sum += values[j];
228
+ }
229
+ const mean = sum / windowLen;
230
+ let sumSq = 0;
231
+ for (let j = start; j < end; j++) {
232
+ const diff = values[j] - mean;
233
+ sumSq += diff * diff;
234
+ }
235
+ const std = Math.sqrt(sumSq / windowLen);
236
+ const adaptiveThresh = mean + thresholdMultiplier * std;
237
+ thresholdCurve[i] = Math.max(0, Math.min(1, (adaptiveThresh - minVal) / range));
238
+ }
239
+ return thresholdCurve;
240
+ }
241
+ function pickPeaksAdaptive(times, values, windowSize = 20, params = {}, includeThresholdCurve = false) {
242
+ const {
243
+ threshold,
244
+ minDistance,
245
+ preMax,
246
+ postMax
247
+ } = { ...DEFAULT_PEAK_PICKING_PARAMS, threshold: 1.5, ...params };
248
+ if (times.length === 0 || values.length === 0) {
249
+ return {
250
+ times: new Float32Array(0),
251
+ strengths: new Float32Array(0)
252
+ };
253
+ }
254
+ const adaptiveThreshold = new Float32Array(values.length);
255
+ const halfWindow = Math.floor(windowSize / 2);
256
+ for (let i = 0; i < values.length; i++) {
257
+ const start = Math.max(0, i - halfWindow);
258
+ const end = Math.min(values.length, i + halfWindow + 1);
259
+ const windowLen = end - start;
260
+ let sum = 0;
261
+ for (let j = start; j < end; j++) {
262
+ sum += values[j];
263
+ }
264
+ const mean = sum / windowLen;
265
+ let sumSq = 0;
266
+ for (let j = start; j < end; j++) {
267
+ const diff = values[j] - mean;
268
+ sumSq += diff * diff;
269
+ }
270
+ const std = Math.sqrt(sumSq / windowLen);
271
+ adaptiveThreshold[i] = mean + threshold * std;
272
+ }
273
+ let minVal = values[0];
274
+ let maxVal = values[0];
275
+ for (let i = 1; i < values.length; i++) {
276
+ const v = values[i];
277
+ if (v < minVal) minVal = v;
278
+ if (v > maxVal) maxVal = v;
279
+ }
280
+ const range = maxVal - minVal;
281
+ const normalize = range > 0 ? (v) => (v - minVal) / range : () => 0.5;
282
+ const peakIndices = [];
283
+ const peakStrengths = [];
284
+ for (let i = preMax; i < values.length - postMax; i++) {
285
+ const currentVal = values[i];
286
+ if (currentVal < adaptiveThreshold[i]) continue;
287
+ let isMax = true;
288
+ for (let j = 1; j <= preMax; j++) {
289
+ if (values[i - j] >= currentVal) {
290
+ isMax = false;
291
+ break;
292
+ }
293
+ }
294
+ if (isMax) {
295
+ for (let j = 1; j <= postMax; j++) {
296
+ if (values[i + j] > currentVal) {
297
+ isMax = false;
298
+ break;
299
+ }
300
+ }
301
+ }
302
+ if (isMax) {
303
+ peakIndices.push(i);
304
+ peakStrengths.push(normalize(currentVal));
305
+ }
306
+ }
307
+ const filteredIndices = [];
308
+ const filteredStrengths = [];
309
+ for (let i = 0; i < peakIndices.length; i++) {
310
+ const idx = peakIndices[i];
311
+ const time = times[idx];
312
+ const strength = peakStrengths[i];
313
+ let shouldKeep = true;
314
+ for (let j = 0; j < filteredIndices.length; j++) {
315
+ const prevIdx = filteredIndices[j];
316
+ const prevTime = times[prevIdx];
317
+ const prevStrength = filteredStrengths[j];
318
+ if (Math.abs(time - prevTime) < minDistance) {
319
+ if (strength > prevStrength) {
320
+ filteredIndices[j] = idx;
321
+ filteredStrengths[j] = strength;
322
+ }
323
+ shouldKeep = false;
324
+ break;
325
+ }
326
+ }
327
+ if (shouldKeep) {
328
+ filteredIndices.push(idx);
329
+ filteredStrengths.push(strength);
330
+ }
331
+ }
332
+ const resultTimes = new Float32Array(filteredIndices.length);
333
+ const resultStrengths = new Float32Array(filteredStrengths.length);
334
+ for (let i = 0; i < filteredIndices.length; i++) {
335
+ resultTimes[i] = times[filteredIndices[i]];
336
+ resultStrengths[i] = filteredStrengths[i];
337
+ }
338
+ const baseResult = {
339
+ times: resultTimes,
340
+ strengths: resultStrengths
341
+ };
342
+ if (!includeThresholdCurve) {
343
+ return baseResult;
344
+ }
345
+ const normalizedThreshold = new Float32Array(values.length);
346
+ for (let i = 0; i < values.length; i++) {
347
+ normalizedThreshold[i] = range > 0 ? Math.max(0, Math.min(1, (adaptiveThreshold[i] - minVal) / range)) : 0.5;
348
+ }
349
+ return {
350
+ ...baseResult,
351
+ thresholdCurve: normalizedThreshold,
352
+ thresholdTimes: times
353
+ };
354
+ }
355
+ function applyHysteresisGate(times, values, peakTimes, peakStrengths, params) {
356
+ const { onThreshold, offThreshold, minDistance } = params;
357
+ if (peakTimes.length === 0) {
358
+ return { times: new Float32Array(0), strengths: new Float32Array(0) };
359
+ }
360
+ let minVal = values[0];
361
+ let maxVal = values[0];
362
+ for (let i = 1; i < values.length; i++) {
363
+ const v = values[i];
364
+ if (v < minVal) minVal = v;
365
+ if (v > maxVal) maxVal = v;
366
+ }
367
+ const range = maxVal - minVal;
368
+ const normalize = range > 0 ? (v) => (v - minVal) / range : () => 0.5;
369
+ const getIndexForTime = (t) => {
370
+ let lo = 0;
371
+ let hi = times.length - 1;
372
+ while (lo < hi) {
373
+ const mid = Math.floor((lo + hi) / 2);
374
+ if (times[mid] < t) {
375
+ lo = mid + 1;
376
+ } else {
377
+ hi = mid;
378
+ }
379
+ }
380
+ return lo;
381
+ };
382
+ const filteredTimes = [];
383
+ const filteredStrengths = [];
384
+ let lastPeakTime = -Infinity;
385
+ let gateOpen = true;
386
+ for (let i = 0; i < peakTimes.length; i++) {
387
+ const peakTime = peakTimes[i];
388
+ const peakStrength = peakStrengths[i];
389
+ if (peakTime - lastPeakTime < minDistance) {
390
+ continue;
391
+ }
392
+ if (!gateOpen) {
393
+ const startIdx = getIndexForTime(lastPeakTime);
394
+ const endIdx = getIndexForTime(peakTime);
395
+ for (let j = startIdx; j < endIdx && j < values.length; j++) {
396
+ if (normalize(values[j]) < offThreshold) {
397
+ gateOpen = true;
398
+ break;
399
+ }
400
+ }
401
+ }
402
+ if (gateOpen && peakStrength >= onThreshold) {
403
+ filteredTimes.push(peakTime);
404
+ filteredStrengths.push(peakStrength);
405
+ lastPeakTime = peakTime;
406
+ gateOpen = false;
407
+ }
408
+ }
409
+ return {
410
+ times: new Float32Array(filteredTimes),
411
+ strengths: new Float32Array(filteredStrengths)
412
+ };
413
+ }
414
+
107
415
  // src/dsp/musicalTime.ts
108
416
  function findSegmentAtTime(time, segments) {
109
417
  for (const segment of segments) {
@@ -315,7 +623,7 @@ function validateBandStructure(structure) {
315
623
  function createBandStructure() {
316
624
  const now = (/* @__PURE__ */ new Date()).toISOString();
317
625
  return {
318
- version: 1,
626
+ version: 2,
319
627
  bands: [],
320
628
  createdAt: now,
321
629
  modifiedAt: now
@@ -326,6 +634,7 @@ function createConstantBand(label, lowHz, highHz, duration, options) {
326
634
  return {
327
635
  id: options?.id ?? generateBandId(),
328
636
  label,
637
+ sourceId: options?.sourceId ?? "mixdown",
329
638
  enabled: options?.enabled ?? true,
330
639
  timeScope: { kind: "global" },
331
640
  frequencyShape: [
@@ -350,6 +659,7 @@ function createSectionedBand(label, lowHz, highHz, startTime, endTime, options)
350
659
  return {
351
660
  id: options?.id ?? generateBandId(),
352
661
  label,
662
+ sourceId: options?.sourceId ?? "mixdown",
353
663
  enabled: options?.enabled ?? true,
354
664
  timeScope: { kind: "sectioned", startTime, endTime },
355
665
  frequencyShape: [
@@ -369,7 +679,7 @@ function createSectionedBand(label, lowHz, highHz, startTime, endTime, options)
369
679
  }
370
680
  };
371
681
  }
372
- function createStandardBands(duration) {
682
+ function createStandardBands(duration, sourceId = "mixdown") {
373
683
  const now = (/* @__PURE__ */ new Date()).toISOString();
374
684
  const bands = [
375
685
  { label: "Sub Bass", lowHz: 20, highHz: 60 },
@@ -382,6 +692,7 @@ function createStandardBands(duration) {
382
692
  return bands.map((b, index) => ({
383
693
  id: generateBandId(),
384
694
  label: b.label,
695
+ sourceId,
385
696
  enabled: true,
386
697
  timeScope: { kind: "global" },
387
698
  frequencyShape: [
@@ -402,10 +713,11 @@ function createStandardBands(duration) {
402
713
  }
403
714
  }));
404
715
  }
405
- function bandsActiveAt(structure, time) {
716
+ function bandsActiveAt(structure, time, sourceId) {
406
717
  if (!structure) return [];
407
718
  return structure.bands.filter((band) => {
408
719
  if (!band.enabled) return false;
720
+ if (sourceId !== void 0 && band.sourceId !== sourceId) return false;
409
721
  if (band.timeScope.kind === "global") return true;
410
722
  return time >= band.timeScope.startTime && time < band.timeScope.endTime;
411
723
  });
@@ -991,6 +1303,44 @@ function bandSpectralFlux(spec, band, options) {
991
1303
  diagnostics: computeDiagnostics(masked.energyRetainedPerFrame)
992
1304
  };
993
1305
  }
1306
+ function bandSpectralCentroid(spec, band, options) {
1307
+ const startMs = typeof performance !== "undefined" ? performance.now() : Date.now();
1308
+ const masked = applyBandMaskToSpectrogram(spec, band, {
1309
+ edgeSmoothHz: options?.edgeSmoothHz
1310
+ });
1311
+ const nFrames = masked.times.length;
1312
+ const nBins = (masked.fftSize >>> 1) + 1;
1313
+ const binHz = masked.sampleRate / masked.fftSize;
1314
+ const out = new Float32Array(nFrames);
1315
+ for (let t = 0; t < nFrames; t++) {
1316
+ const mags = masked.magnitudes[t];
1317
+ if (!mags) {
1318
+ out[t] = 0;
1319
+ continue;
1320
+ }
1321
+ let num = 0;
1322
+ let den = 0;
1323
+ for (let k = 0; k < nBins; k++) {
1324
+ const m = mags[k] ?? 0;
1325
+ if (m > 0) {
1326
+ const f = k * binHz;
1327
+ num += f * m;
1328
+ den += m;
1329
+ }
1330
+ }
1331
+ out[t] = den > 0 ? num / den : 0;
1332
+ }
1333
+ return {
1334
+ kind: "bandMir1d",
1335
+ bandId: band.id,
1336
+ bandLabel: band.label,
1337
+ fn: "bandSpectralCentroid",
1338
+ times: masked.times,
1339
+ values: out,
1340
+ meta: createMeta(startMs),
1341
+ diagnostics: computeDiagnostics(masked.energyRetainedPerFrame)
1342
+ };
1343
+ }
994
1344
  async function runBandMirBatch(spec, request, options) {
995
1345
  const startMs = typeof performance !== "undefined" ? performance.now() : Date.now();
996
1346
  const results = /* @__PURE__ */ new Map();
@@ -1015,6 +1365,9 @@ async function runBandMirBatch(spec, request, options) {
1015
1365
  case "bandSpectralFlux":
1016
1366
  result = bandSpectralFlux(spec, band, options);
1017
1367
  break;
1368
+ case "bandSpectralCentroid":
1369
+ result = bandSpectralCentroid(spec, band, options);
1370
+ break;
1018
1371
  default:
1019
1372
  const _exhaustive = fn;
1020
1373
  throw new Error(`Unknown band MIR function: ${_exhaustive}`);
@@ -1037,93 +1390,670 @@ function getBandMirFunctionLabel(fn) {
1037
1390
  return "Onset Strength";
1038
1391
  case "bandSpectralFlux":
1039
1392
  return "Spectral Flux";
1393
+ case "bandSpectralCentroid":
1394
+ return "Spectral Centroid";
1040
1395
  default:
1041
1396
  return fn;
1042
1397
  }
1043
1398
  }
1044
1399
 
1045
- // src/dsp/bandProposal.ts
1046
- var PROPOSAL_DEFAULTS = {
1047
- maxProposals: 8,
1048
- minSalience: 0.3,
1049
- minSeparationOctaves: 0.5,
1050
- analysisWindow: 0
1051
- // 0 = full track
1052
- };
1053
- function generateProposalId() {
1054
- return `proposal-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1400
+ // src/dsp/bandEvents.ts
1401
+ function nowMs() {
1402
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
1055
1403
  }
1056
- function generateBandId2() {
1057
- return `band-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1404
+ function createMeta2(startMs) {
1405
+ const endMs = nowMs();
1406
+ const timings = {
1407
+ totalMs: endMs - startMs,
1408
+ cpuMs: endMs - startMs,
1409
+ gpuMs: 0
1410
+ };
1411
+ return {
1412
+ backend: "cpu",
1413
+ usedGpu: false,
1414
+ timings
1415
+ };
1058
1416
  }
1059
- function computeAverageCqtSpectrum(cqt) {
1060
- const nBins = cqt.magnitudes[0]?.length ?? 0;
1061
- const nFrames = cqt.magnitudes.length;
1062
- const average = new Float32Array(nBins);
1063
- for (let frame = 0; frame < nFrames; frame++) {
1064
- const frameMags = cqt.magnitudes[frame];
1065
- if (!frameMags) continue;
1066
- for (let bin = 0; bin < nBins; bin++) {
1067
- average[bin] = (average[bin] ?? 0) + (frameMags[bin] ?? 0);
1068
- }
1417
+ function computeEventDiagnostics(events, durationSec) {
1418
+ const eventCount = events.length;
1419
+ const eventsPerSecond = durationSec > 0 ? eventCount / durationSec : 0;
1420
+ const warnings = [];
1421
+ if (durationSec > 1 && eventCount === 0) {
1422
+ warnings.push("No events detected - signal may be too quiet or noisy");
1423
+ } else if (eventsPerSecond > 20) {
1424
+ warnings.push("Very high event density (>20/sec) - consider adjusting threshold");
1425
+ } else if (durationSec > 10 && eventsPerSecond < 0.1) {
1426
+ warnings.push("Very sparse events (<0.1/sec) - signal may not be active");
1069
1427
  }
1070
- for (let bin = 0; bin < nBins; bin++) {
1071
- average[bin] = (average[bin] ?? 0) / nFrames;
1428
+ return {
1429
+ eventCount,
1430
+ eventsPerSecond,
1431
+ warnings
1432
+ };
1433
+ }
1434
+ function bandOnsetPeaks(signal, options) {
1435
+ const startMs = nowMs();
1436
+ const minIntervalSec = options?.minIntervalSec ?? 0.0625;
1437
+ const adaptiveFactor = options?.adaptiveFactor ?? 0.8;
1438
+ const strict = options?.strict ?? true;
1439
+ const { times, values } = signal;
1440
+ const pickOptions = {
1441
+ minIntervalSec,
1442
+ adaptive: {
1443
+ method: "meanStd",
1444
+ factor: adaptiveFactor
1445
+ },
1446
+ strict
1447
+ };
1448
+ const peaks = peakPick(times, values, pickOptions);
1449
+ const maxStrength = peaks.reduce((max, p) => Math.max(max, p.strength), 0);
1450
+ const events = peaks.map((p) => ({
1451
+ time: p.time,
1452
+ weight: maxStrength > 0 ? p.strength / maxStrength : 1
1453
+ }));
1454
+ const duration = times.length >= 2 ? (times[times.length - 1] ?? 0) - (times[0] ?? 0) : 0;
1455
+ return {
1456
+ kind: "bandEvents",
1457
+ bandId: signal.bandId,
1458
+ bandLabel: signal.bandLabel,
1459
+ fn: "bandOnsetPeaks",
1460
+ events,
1461
+ sourceSignal: {
1462
+ fn: signal.fn,
1463
+ times: signal.times,
1464
+ values: signal.values
1465
+ },
1466
+ meta: createMeta2(startMs),
1467
+ diagnostics: computeEventDiagnostics(events, duration)
1468
+ };
1469
+ }
1470
+ function bandBeatCandidates(onsetPeaks, options) {
1471
+ const startMs = nowMs();
1472
+ const minIntervalSec = options?.minIntervalSec ?? 0.1;
1473
+ const thresholdFactor = options?.thresholdFactor ?? 0.5;
1474
+ const weights = onsetPeaks.events.map((e) => e.weight);
1475
+ const meanWeight = weights.length > 0 ? weights.reduce((sum, w) => sum + w, 0) / weights.length : 0;
1476
+ const variance = weights.length > 0 ? weights.reduce((sum, w) => sum + (w - meanWeight) ** 2, 0) / weights.length : 0;
1477
+ const stdWeight = Math.sqrt(variance);
1478
+ const threshold = meanWeight + thresholdFactor * stdWeight;
1479
+ const candidates = [];
1480
+ let lastTime = -Infinity;
1481
+ for (const event of onsetPeaks.events) {
1482
+ if (event.weight < threshold) continue;
1483
+ if (event.time - lastTime < minIntervalSec) {
1484
+ const last = candidates[candidates.length - 1];
1485
+ if (last && event.weight > last.weight) {
1486
+ last.time = event.time;
1487
+ last.weight = event.weight;
1488
+ lastTime = event.time;
1489
+ }
1490
+ continue;
1491
+ }
1492
+ candidates.push({
1493
+ time: event.time,
1494
+ weight: event.weight,
1495
+ beatPosition: event.beatPosition,
1496
+ beatPhase: event.beatPhase
1497
+ });
1498
+ lastTime = event.time;
1072
1499
  }
1073
- return average;
1500
+ 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;
1501
+ return {
1502
+ kind: "bandEvents",
1503
+ bandId: onsetPeaks.bandId,
1504
+ bandLabel: onsetPeaks.bandLabel,
1505
+ fn: "bandBeatCandidates",
1506
+ events: candidates,
1507
+ // Don't include source signal to save memory
1508
+ meta: createMeta2(startMs),
1509
+ diagnostics: computeEventDiagnostics(candidates, duration)
1510
+ };
1074
1511
  }
1075
- function findLocalMaxima(values, minNeighborDistance = 3) {
1076
- const peaks = [];
1077
- for (let i = minNeighborDistance; i < values.length - minNeighborDistance; i++) {
1078
- let isPeak = true;
1079
- const centerVal = values[i] ?? 0;
1080
- for (let j = -minNeighborDistance; j <= minNeighborDistance; j++) {
1081
- if (j === 0) continue;
1082
- if ((values[i + j] ?? 0) >= centerVal) {
1083
- isPeak = false;
1084
- break;
1512
+ async function runBandEventsBatch(request) {
1513
+ const startMs = nowMs();
1514
+ const results = /* @__PURE__ */ new Map();
1515
+ const sourceFunction = request.sourceFunction ?? "bandOnsetStrength";
1516
+ for (const [bandId, mirResults] of request.bandMirResults.entries()) {
1517
+ const bandEventResults = [];
1518
+ const sourceSignal = mirResults.find((r) => r.fn === sourceFunction);
1519
+ if (!sourceSignal) {
1520
+ continue;
1521
+ }
1522
+ for (const fn of request.functions) {
1523
+ switch (fn) {
1524
+ case "bandOnsetPeaks": {
1525
+ const result = bandOnsetPeaks(sourceSignal, request.onsetPeaksOptions);
1526
+ bandEventResults.push(result);
1527
+ break;
1528
+ }
1529
+ case "bandBeatCandidates": {
1530
+ let onsetPeaksResult = bandEventResults.find(
1531
+ (r) => r.fn === "bandOnsetPeaks"
1532
+ );
1533
+ if (!onsetPeaksResult) {
1534
+ onsetPeaksResult = bandOnsetPeaks(
1535
+ sourceSignal,
1536
+ request.onsetPeaksOptions
1537
+ );
1538
+ if (!request.functions.includes("bandOnsetPeaks")) ; else {
1539
+ bandEventResults.push(onsetPeaksResult);
1540
+ }
1541
+ }
1542
+ const result = bandBeatCandidates(
1543
+ onsetPeaksResult,
1544
+ request.beatCandidatesOptions
1545
+ );
1546
+ bandEventResults.push(result);
1547
+ break;
1548
+ }
1549
+ default: {
1550
+ const _exhaustive = fn;
1551
+ throw new Error(`Unknown band event function: ${_exhaustive}`);
1552
+ }
1085
1553
  }
1086
1554
  }
1087
- if (isPeak && centerVal > 0) {
1088
- peaks.push(i);
1555
+ if (bandEventResults.length > 0) {
1556
+ results.set(bandId, bandEventResults);
1089
1557
  }
1090
1558
  }
1091
- return peaks;
1559
+ const endMs = nowMs();
1560
+ return {
1561
+ results,
1562
+ totalTimingMs: endMs - startMs
1563
+ };
1092
1564
  }
1093
- function computeBandwidth(values, peakBin, cqt) {
1094
- const peakMag = values[peakBin] ?? 0;
1095
- const threshold = peakMag * 0.707;
1096
- let lowBin = peakBin;
1097
- while (lowBin > 0 && (values[lowBin - 1] ?? 0) >= threshold) {
1098
- lowBin--;
1565
+ function getBandEventFunctionLabel(fn) {
1566
+ switch (fn) {
1567
+ case "bandOnsetPeaks":
1568
+ return "Onset Peaks";
1569
+ case "bandBeatCandidates":
1570
+ return "Beat Candidates";
1571
+ default:
1572
+ return fn;
1099
1573
  }
1100
- let highBin = peakBin;
1101
- while (highBin < values.length - 1 && (values[highBin + 1] ?? 0) >= threshold) {
1102
- highBin++;
1574
+ }
1575
+
1576
+ // src/dsp/bandCqt.ts
1577
+ function nowMs2() {
1578
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
1579
+ }
1580
+ function createMeta3(startMs) {
1581
+ const endMs = nowMs2();
1582
+ const timings = {
1583
+ totalMs: endMs - startMs,
1584
+ cpuMs: endMs - startMs,
1585
+ gpuMs: 0
1586
+ };
1587
+ return {
1588
+ backend: "cpu",
1589
+ usedGpu: false,
1590
+ timings
1591
+ };
1592
+ }
1593
+ function computeDiagnostics2(energyRetainedPerFrame) {
1594
+ const totalFrames = energyRetainedPerFrame.length;
1595
+ let sum = 0;
1596
+ let weakCount = 0;
1597
+ let emptyCount = 0;
1598
+ for (let i = 0; i < totalFrames; i++) {
1599
+ const e = energyRetainedPerFrame[i] ?? 0;
1600
+ sum += e;
1601
+ if (e < 0.01) weakCount++;
1602
+ if (e === 0) emptyCount++;
1103
1603
  }
1104
- const lowHz = cqtBinToHz(lowBin, cqt.config);
1105
- const highHz = cqtBinToHz(highBin, cqt.config);
1106
- const bandwidthOctaves = Math.log2(highHz / lowHz);
1107
- return { lowBin, highBin, lowHz, highHz, bandwidthOctaves };
1604
+ const meanEnergyRetained = totalFrames > 0 ? sum / totalFrames : 0;
1605
+ const warnings = [];
1606
+ if (meanEnergyRetained < 0.01) {
1607
+ warnings.push("Band contains very little CQT energy - check frequency range");
1608
+ }
1609
+ if (emptyCount > totalFrames * 0.5) {
1610
+ warnings.push("More than half of frames are empty in CQT - band may not be active");
1611
+ }
1612
+ return {
1613
+ meanEnergyRetained,
1614
+ weakFrameCount: weakCount,
1615
+ emptyFrameCount: emptyCount,
1616
+ totalFrames,
1617
+ warnings
1618
+ };
1108
1619
  }
1109
- function computeTemporalVariance(cqt, lowBin, highBin) {
1110
- const nFrames = cqt.magnitudes.length;
1111
- const bandEnergies = new Float32Array(nFrames);
1112
- for (let frame = 0; frame < nFrames; frame++) {
1113
- const frameMags = cqt.magnitudes[frame];
1114
- if (!frameMags) continue;
1115
- let energy = 0;
1116
- for (let bin = lowBin; bin <= highBin; bin++) {
1117
- const mag = frameMags[bin] ?? 0;
1118
- energy += mag * mag;
1620
+ function getBandBoundsAtTime(band, timeSec) {
1621
+ for (const seg of band.frequencyShape) {
1622
+ if (timeSec >= seg.startTime && timeSec < seg.endTime) {
1623
+ const ratio = (timeSec - seg.startTime) / (seg.endTime - seg.startTime);
1624
+ const lowHz = seg.lowHzStart + ratio * (seg.lowHzEnd - seg.lowHzStart);
1625
+ const highHz = seg.highHzStart + ratio * (seg.highHzEnd - seg.highHzStart);
1626
+ return { lowHz, highHz };
1119
1627
  }
1120
- bandEnergies[frame] = energy;
1121
1628
  }
1122
- let sum = 0;
1123
- for (let i = 0; i < nFrames; i++) {
1124
- sum += bandEnergies[i] ?? 0;
1629
+ const first = band.frequencyShape[0];
1630
+ if (first) {
1631
+ return { lowHz: first.lowHzStart, highHz: first.highHzStart };
1125
1632
  }
1126
- const mean = sum / nFrames;
1633
+ return { lowHz: 20, highHz: 2e4 };
1634
+ }
1635
+ function applyBandMaskToCqt(cqt, band, options) {
1636
+ const edgeSmoothBins = options?.edgeSmoothBins ?? 0;
1637
+ const nFrames = cqt.times.length;
1638
+ const nBins = cqt.binFrequencies.length;
1639
+ const maskedMagnitudes = new Array(nFrames);
1640
+ const energyRetainedPerFrame = new Float32Array(nFrames);
1641
+ for (let t = 0; t < nFrames; t++) {
1642
+ const frame = cqt.magnitudes[t];
1643
+ if (!frame) {
1644
+ maskedMagnitudes[t] = new Float32Array(nBins);
1645
+ energyRetainedPerFrame[t] = 0;
1646
+ continue;
1647
+ }
1648
+ const timeSec = cqt.times[t] ?? 0;
1649
+ const { lowHz, highHz } = getBandBoundsAtTime(band, timeSec);
1650
+ const lowBin = Math.max(0, Math.floor(hzToCqtBin(lowHz, cqt.config)));
1651
+ const highBin = Math.min(nBins, Math.ceil(hzToCqtBin(highHz, cqt.config)));
1652
+ const maskedFrame = new Float32Array(nBins);
1653
+ let originalEnergy = 0;
1654
+ let retainedEnergy = 0;
1655
+ for (let k = 0; k < nBins; k++) {
1656
+ const mag = frame[k] ?? 0;
1657
+ originalEnergy += mag * mag;
1658
+ if (k >= lowBin && k < highBin) {
1659
+ let weight = 1;
1660
+ if (edgeSmoothBins > 0) {
1661
+ const distFromLow = k - lowBin;
1662
+ const distFromHigh = highBin - 1 - k;
1663
+ const minDist = Math.min(distFromLow, distFromHigh);
1664
+ if (minDist < edgeSmoothBins) {
1665
+ weight = (minDist + 1) / (edgeSmoothBins + 1);
1666
+ }
1667
+ }
1668
+ maskedFrame[k] = mag * weight;
1669
+ retainedEnergy += (mag * weight) ** 2;
1670
+ }
1671
+ }
1672
+ maskedMagnitudes[t] = maskedFrame;
1673
+ energyRetainedPerFrame[t] = originalEnergy > 0 ? Math.sqrt(retainedEnergy / originalEnergy) : 0;
1674
+ }
1675
+ return {
1676
+ times: cqt.times,
1677
+ magnitudes: maskedMagnitudes,
1678
+ config: cqt.config,
1679
+ energyRetainedPerFrame,
1680
+ binFrequencies: cqt.binFrequencies
1681
+ };
1682
+ }
1683
+ function bandCqtHarmonicEnergy(cqt, band, options) {
1684
+ const startMs = nowMs2();
1685
+ const masked = applyBandMaskToCqt(cqt, band, options);
1686
+ const nFrames = masked.times.length;
1687
+ const values = new Float32Array(nFrames);
1688
+ for (let t = 0; t < nFrames; t++) {
1689
+ if (options?.isCancelled?.()) {
1690
+ throw new Error("@octoseq/mir: cancelled");
1691
+ }
1692
+ const frame = masked.magnitudes[t];
1693
+ if (!frame) {
1694
+ values[t] = 0;
1695
+ continue;
1696
+ }
1697
+ let totalEnergy = 0;
1698
+ for (let k = 0; k < frame.length; k++) {
1699
+ const mag = frame[k] ?? 0;
1700
+ totalEnergy += mag * mag;
1701
+ }
1702
+ if (totalEnergy === 0) {
1703
+ values[t] = 0;
1704
+ continue;
1705
+ }
1706
+ let maxMag = 0;
1707
+ let fundamentalBin = 0;
1708
+ for (let k = 0; k < frame.length; k++) {
1709
+ const mag = frame[k] ?? 0;
1710
+ if (mag > maxMag) {
1711
+ maxMag = mag;
1712
+ fundamentalBin = k;
1713
+ }
1714
+ }
1715
+ const fundamentalFreq = cqtBinToHz(fundamentalBin, cqt.config);
1716
+ let harmonicEnergy2 = 0;
1717
+ const numHarmonics = 6;
1718
+ for (let h = 1; h <= numHarmonics; h++) {
1719
+ const harmonicFreq = fundamentalFreq * h;
1720
+ const harmonicBin = Math.round(hzToCqtBin(harmonicFreq, cqt.config));
1721
+ if (harmonicBin >= 0 && harmonicBin < frame.length) {
1722
+ const mag = frame[harmonicBin] ?? 0;
1723
+ const weight = 1 / h;
1724
+ harmonicEnergy2 += mag * mag * weight;
1725
+ }
1726
+ }
1727
+ let weightSum = 0;
1728
+ for (let h = 1; h <= numHarmonics; h++) {
1729
+ weightSum += 1 / h;
1730
+ }
1731
+ harmonicEnergy2 /= weightSum;
1732
+ values[t] = Math.min(1, harmonicEnergy2 / totalEnergy);
1733
+ }
1734
+ const normalized = normalizeMinMax(values);
1735
+ return {
1736
+ kind: "bandCqt1d",
1737
+ bandId: band.id,
1738
+ bandLabel: band.label,
1739
+ fn: "bandCqtHarmonicEnergy",
1740
+ times: masked.times,
1741
+ values: normalized,
1742
+ meta: createMeta3(startMs),
1743
+ diagnostics: computeDiagnostics2(masked.energyRetainedPerFrame)
1744
+ };
1745
+ }
1746
+ function bandCqtBassPitchMotion(cqt, band, options) {
1747
+ const startMs = nowMs2();
1748
+ const masked = applyBandMaskToCqt(cqt, band, options);
1749
+ const nFrames = masked.times.length;
1750
+ const BASS_MIN_HZ = 20;
1751
+ const BASS_MAX_HZ = 300;
1752
+ const bassStartBin = Math.max(0, Math.floor(hzToCqtBin(BASS_MIN_HZ, cqt.config)));
1753
+ const bassEndBin = Math.min(
1754
+ masked.binFrequencies.length,
1755
+ Math.ceil(hzToCqtBin(BASS_MAX_HZ, cqt.config))
1756
+ );
1757
+ const bassNumBins = bassEndBin - bassStartBin;
1758
+ if (bassNumBins <= 0) {
1759
+ return {
1760
+ kind: "bandCqt1d",
1761
+ bandId: band.id,
1762
+ bandLabel: band.label,
1763
+ fn: "bandCqtBassPitchMotion",
1764
+ times: masked.times,
1765
+ values: new Float32Array(nFrames),
1766
+ meta: createMeta3(startMs),
1767
+ diagnostics: {
1768
+ ...computeDiagnostics2(masked.energyRetainedPerFrame),
1769
+ warnings: ["Band does not overlap bass frequency range (20-300 Hz)"]
1770
+ }
1771
+ };
1772
+ }
1773
+ const centroids = new Float32Array(nFrames);
1774
+ for (let t = 0; t < nFrames; t++) {
1775
+ const frame = masked.magnitudes[t];
1776
+ if (!frame) continue;
1777
+ let num = 0;
1778
+ let den = 0;
1779
+ for (let k = bassStartBin; k < bassEndBin; k++) {
1780
+ const mag = frame[k] ?? 0;
1781
+ if (mag > 0) {
1782
+ num += k * mag;
1783
+ den += mag;
1784
+ }
1785
+ }
1786
+ centroids[t] = den > 0 ? num / den : bassStartBin + bassNumBins / 2;
1787
+ }
1788
+ const motion = new Float32Array(nFrames);
1789
+ for (let t = 1; t < nFrames; t++) {
1790
+ motion[t] = Math.abs((centroids[t] ?? 0) - (centroids[t - 1] ?? 0));
1791
+ }
1792
+ if (nFrames > 1) {
1793
+ motion[0] = motion[1] ?? 0;
1794
+ }
1795
+ const normalized = normalizeMinMax(motion);
1796
+ return {
1797
+ kind: "bandCqt1d",
1798
+ bandId: band.id,
1799
+ bandLabel: band.label,
1800
+ fn: "bandCqtBassPitchMotion",
1801
+ times: masked.times,
1802
+ values: normalized,
1803
+ meta: createMeta3(startMs),
1804
+ diagnostics: computeDiagnostics2(masked.energyRetainedPerFrame)
1805
+ };
1806
+ }
1807
+ function bandCqtTonalStability(cqt, band, options) {
1808
+ const startMs = nowMs2();
1809
+ const masked = applyBandMaskToCqt(cqt, band, options);
1810
+ const nFrames = masked.times.length;
1811
+ const CHROMA_BINS = 12;
1812
+ const WINDOW_FRAMES = 20;
1813
+ const chromas = new Array(nFrames);
1814
+ for (let t = 0; t < nFrames; t++) {
1815
+ const frame = masked.magnitudes[t];
1816
+ const chroma = new Float32Array(CHROMA_BINS);
1817
+ if (frame) {
1818
+ const binsPerSemitone = cqt.binsPerOctave / CHROMA_BINS;
1819
+ for (let k = 0; k < frame.length; k++) {
1820
+ const chromaBin = Math.floor(k % cqt.binsPerOctave / binsPerSemitone) % CHROMA_BINS;
1821
+ const mag = frame[k] ?? 0;
1822
+ chroma[chromaBin] = (chroma[chromaBin] ?? 0) + mag * mag;
1823
+ }
1824
+ let sum = 0;
1825
+ for (let c = 0; c < CHROMA_BINS; c++) {
1826
+ sum += chroma[c] ?? 0;
1827
+ }
1828
+ if (sum > 0) {
1829
+ for (let c = 0; c < CHROMA_BINS; c++) {
1830
+ chroma[c] = (chroma[c] ?? 0) / sum;
1831
+ }
1832
+ }
1833
+ }
1834
+ chromas[t] = chroma;
1835
+ }
1836
+ const halfWindow = Math.floor(WINDOW_FRAMES / 2);
1837
+ const instability = new Float32Array(nFrames);
1838
+ for (let t = 0; t < nFrames; t++) {
1839
+ const windowStart = Math.max(0, t - halfWindow);
1840
+ const windowEnd = Math.min(nFrames, t + halfWindow + 1);
1841
+ const windowSize = windowEnd - windowStart;
1842
+ const avgChroma = new Float32Array(CHROMA_BINS);
1843
+ for (let w = windowStart; w < windowEnd; w++) {
1844
+ const chroma = chromas[w];
1845
+ if (chroma) {
1846
+ for (let c = 0; c < CHROMA_BINS; c++) {
1847
+ avgChroma[c] = (avgChroma[c] ?? 0) + (chroma[c] ?? 0);
1848
+ }
1849
+ }
1850
+ }
1851
+ for (let c = 0; c < CHROMA_BINS; c++) {
1852
+ avgChroma[c] = (avgChroma[c] ?? 0) / windowSize;
1853
+ }
1854
+ let totalVariance = 0;
1855
+ for (let w = windowStart; w < windowEnd; w++) {
1856
+ const chroma = chromas[w];
1857
+ if (chroma) {
1858
+ for (let c = 0; c < CHROMA_BINS; c++) {
1859
+ const diff = (chroma[c] ?? 0) - (avgChroma[c] ?? 0);
1860
+ totalVariance += diff * diff;
1861
+ }
1862
+ }
1863
+ }
1864
+ totalVariance /= windowSize * CHROMA_BINS;
1865
+ instability[t] = totalVariance;
1866
+ }
1867
+ const normalizedInstability = normalizeMinMax(instability);
1868
+ const stability = new Float32Array(nFrames);
1869
+ for (let t = 0; t < nFrames; t++) {
1870
+ stability[t] = 1 - (normalizedInstability[t] ?? 0);
1871
+ }
1872
+ return {
1873
+ kind: "bandCqt1d",
1874
+ bandId: band.id,
1875
+ bandLabel: band.label,
1876
+ fn: "bandCqtTonalStability",
1877
+ times: masked.times,
1878
+ values: stability,
1879
+ meta: createMeta3(startMs),
1880
+ diagnostics: computeDiagnostics2(masked.energyRetainedPerFrame)
1881
+ };
1882
+ }
1883
+ function normalizeMinMax(values) {
1884
+ const n = values.length;
1885
+ if (n === 0) return new Float32Array(0);
1886
+ let min = Infinity;
1887
+ let max = -Infinity;
1888
+ for (let i = 0; i < n; i++) {
1889
+ const v = values[i] ?? 0;
1890
+ if (v < min) min = v;
1891
+ if (v > max) max = v;
1892
+ }
1893
+ const out = new Float32Array(n);
1894
+ const range = max - min;
1895
+ if (range === 0 || !Number.isFinite(range)) {
1896
+ out.fill(0.5);
1897
+ return out;
1898
+ }
1899
+ for (let i = 0; i < n; i++) {
1900
+ out[i] = ((values[i] ?? 0) - min) / range;
1901
+ }
1902
+ return out;
1903
+ }
1904
+ async function runBandCqtBatch(cqt, request, options) {
1905
+ const startMs = nowMs2();
1906
+ const results = /* @__PURE__ */ new Map();
1907
+ for (const band of request.bands) {
1908
+ if (options?.isCancelled?.()) {
1909
+ throw new Error("@octoseq/mir: cancelled");
1910
+ }
1911
+ if (!band.enabled) continue;
1912
+ const bandResults = [];
1913
+ for (const fn of request.functions) {
1914
+ if (options?.isCancelled?.()) {
1915
+ throw new Error("@octoseq/mir: cancelled");
1916
+ }
1917
+ let result;
1918
+ switch (fn) {
1919
+ case "bandCqtHarmonicEnergy":
1920
+ result = bandCqtHarmonicEnergy(cqt, band, options);
1921
+ break;
1922
+ case "bandCqtBassPitchMotion":
1923
+ result = bandCqtBassPitchMotion(cqt, band, options);
1924
+ break;
1925
+ case "bandCqtTonalStability":
1926
+ result = bandCqtTonalStability(cqt, band, options);
1927
+ break;
1928
+ default:
1929
+ const _exhaustive = fn;
1930
+ throw new Error(`Unknown band CQT function: ${_exhaustive}`);
1931
+ }
1932
+ bandResults.push(result);
1933
+ }
1934
+ results.set(band.id, bandResults);
1935
+ }
1936
+ const endMs = nowMs2();
1937
+ return {
1938
+ results,
1939
+ totalTimingMs: endMs - startMs
1940
+ };
1941
+ }
1942
+ function getBandCqtFunctionLabel(fn) {
1943
+ switch (fn) {
1944
+ case "bandCqtHarmonicEnergy":
1945
+ return "Harmonic Energy (CQT)";
1946
+ case "bandCqtBassPitchMotion":
1947
+ return "Bass Pitch Motion (CQT)";
1948
+ case "bandCqtTonalStability":
1949
+ return "Tonal Stability (CQT)";
1950
+ default:
1951
+ return fn;
1952
+ }
1953
+ }
1954
+
1955
+ // src/dsp/bandProposal.ts
1956
+ var PROPOSAL_DEFAULTS = {
1957
+ maxProposals: 8,
1958
+ minSalience: 0.3,
1959
+ minSeparationOctaves: 0.5,
1960
+ minBandwidthHz: 20,
1961
+ analysisWindow: 0
1962
+ // 0 = full track
1963
+ };
1964
+ function generateProposalId() {
1965
+ return `proposal-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1966
+ }
1967
+ function generateBandId2() {
1968
+ return `band-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1969
+ }
1970
+ function computeAverageCqtSpectrum(cqt) {
1971
+ const nBins = cqt.magnitudes[0]?.length ?? 0;
1972
+ const nFrames = cqt.magnitudes.length;
1973
+ const average = new Float32Array(nBins);
1974
+ for (let frame = 0; frame < nFrames; frame++) {
1975
+ const frameMags = cqt.magnitudes[frame];
1976
+ if (!frameMags) continue;
1977
+ for (let bin = 0; bin < nBins; bin++) {
1978
+ average[bin] = (average[bin] ?? 0) + (frameMags[bin] ?? 0);
1979
+ }
1980
+ }
1981
+ for (let bin = 0; bin < nBins; bin++) {
1982
+ average[bin] = (average[bin] ?? 0) / nFrames;
1983
+ }
1984
+ return average;
1985
+ }
1986
+ function findLocalMaxima(values, minNeighborDistance = 3) {
1987
+ const peaks = [];
1988
+ for (let i = minNeighborDistance; i < values.length - minNeighborDistance; i++) {
1989
+ let isPeak = true;
1990
+ const centerVal = values[i] ?? 0;
1991
+ for (let j = -minNeighborDistance; j <= minNeighborDistance; j++) {
1992
+ if (j === 0) continue;
1993
+ if ((values[i + j] ?? 0) >= centerVal) {
1994
+ isPeak = false;
1995
+ break;
1996
+ }
1997
+ }
1998
+ if (isPeak && centerVal > 0) {
1999
+ peaks.push(i);
2000
+ }
2001
+ }
2002
+ return peaks;
2003
+ }
2004
+ function computeBandwidth(values, peakBin, cqt, minBandwidthHz) {
2005
+ const peakMag = values[peakBin] ?? 0;
2006
+ const threshold = peakMag * 0.707;
2007
+ let lowBin = peakBin;
2008
+ while (lowBin > 0 && (values[lowBin - 1] ?? 0) >= threshold) {
2009
+ lowBin--;
2010
+ }
2011
+ let highBin = peakBin;
2012
+ while (highBin < values.length - 1 && (values[highBin + 1] ?? 0) >= threshold) {
2013
+ highBin++;
2014
+ }
2015
+ if (minBandwidthHz > 0) {
2016
+ const maxExpansions = values.length;
2017
+ for (let i = 0; i < maxExpansions; i++) {
2018
+ const lowHzTmp = cqtBinToHz(lowBin, cqt.config);
2019
+ const highHzTmp = cqtBinToHz(highBin, cqt.config);
2020
+ if (highHzTmp - lowHzTmp >= minBandwidthHz) break;
2021
+ const canExpandLow = lowBin > 0;
2022
+ const canExpandHigh = highBin < values.length - 1;
2023
+ if (!canExpandLow && !canExpandHigh) break;
2024
+ if (canExpandLow && canExpandHigh) {
2025
+ lowBin--;
2026
+ highBin++;
2027
+ } else if (canExpandLow) {
2028
+ lowBin--;
2029
+ } else {
2030
+ highBin++;
2031
+ }
2032
+ }
2033
+ }
2034
+ const lowHz = cqtBinToHz(lowBin, cqt.config);
2035
+ const highHz = cqtBinToHz(highBin, cqt.config);
2036
+ const bandwidthOctaves = Math.log2(highHz / lowHz);
2037
+ return { lowBin, highBin, lowHz, highHz, bandwidthOctaves };
2038
+ }
2039
+ function computeTemporalVariance(cqt, lowBin, highBin) {
2040
+ const nFrames = cqt.magnitudes.length;
2041
+ const bandEnergies = new Float32Array(nFrames);
2042
+ for (let frame = 0; frame < nFrames; frame++) {
2043
+ const frameMags = cqt.magnitudes[frame];
2044
+ if (!frameMags) continue;
2045
+ let energy = 0;
2046
+ for (let bin = lowBin; bin <= highBin; bin++) {
2047
+ const mag = frameMags[bin] ?? 0;
2048
+ energy += mag * mag;
2049
+ }
2050
+ bandEnergies[frame] = energy;
2051
+ }
2052
+ let sum = 0;
2053
+ for (let i = 0; i < nFrames; i++) {
2054
+ sum += bandEnergies[i] ?? 0;
2055
+ }
2056
+ const mean = sum / nFrames;
1127
2057
  let variance = 0;
1128
2058
  for (let i = 0; i < nFrames; i++) {
1129
2059
  const diff = (bandEnergies[i] ?? 0) - mean;
@@ -1138,7 +2068,7 @@ function detectSpectralPeaks(cqt, config) {
1138
2068
  const peakIndices = findLocalMaxima(avgSpectrum, Math.max(3, minBinDistance / 2));
1139
2069
  const peaks = [];
1140
2070
  for (const binIndex of peakIndices) {
1141
- const bw = computeBandwidth(avgSpectrum, binIndex, cqt);
2071
+ const bw = computeBandwidth(avgSpectrum, binIndex, cqt, config.minBandwidthHz);
1142
2072
  peaks.push({
1143
2073
  binIndex,
1144
2074
  centerHz: cqtBinToHz(binIndex, cqt.config),
@@ -1230,6 +2160,8 @@ function createBandFromCandidate(candidate, duration) {
1230
2160
  return {
1231
2161
  id: generateBandId2(),
1232
2162
  label: `Region ${Math.round(candidate.peak.centerHz)} Hz`,
2163
+ sourceId: "mixdown",
2164
+ // Proposals default to mixdown; user assigns sourceId on promotion
1233
2165
  enabled: true,
1234
2166
  timeScope: { kind: "global" },
1235
2167
  frequencyShape: [
@@ -1334,6 +2266,479 @@ async function generateBandProposals(audio, duration, options = {}) {
1334
2266
  };
1335
2267
  }
1336
2268
 
2269
+ // src/dsp/customSignalReduction.ts
2270
+ function logCompress2(x) {
2271
+ return Math.log1p(Math.max(0, x));
2272
+ }
2273
+ function movingAverage2(values, windowFrames) {
2274
+ if (windowFrames <= 1) return values;
2275
+ const n = values.length;
2276
+ const out = new Float32Array(n);
2277
+ const half = Math.floor(windowFrames / 2);
2278
+ const prefix = new Float64Array(n + 1);
2279
+ prefix[0] = 0;
2280
+ for (let i = 0; i < n; i++) {
2281
+ prefix[i + 1] = (prefix[i] ?? 0) + (values[i] ?? 0);
2282
+ }
2283
+ for (let i = 0; i < n; i++) {
2284
+ const start = Math.max(0, i - half);
2285
+ const end = Math.min(n, i + half + 1);
2286
+ const sum = (prefix[end] ?? 0) - (prefix[start] ?? 0);
2287
+ const count = Math.max(1, end - start);
2288
+ out[i] = sum / count;
2289
+ }
2290
+ return out;
2291
+ }
2292
+ function computeValueRange(values) {
2293
+ if (values.length === 0) {
2294
+ return { min: 0, max: 0 };
2295
+ }
2296
+ let min = values[0] ?? 0;
2297
+ let max = values[0] ?? 0;
2298
+ for (let i = 1; i < values.length; i++) {
2299
+ const v = values[i] ?? 0;
2300
+ if (v < min) min = v;
2301
+ if (v > max) max = v;
2302
+ }
2303
+ return { min, max };
2304
+ }
2305
+ function getBinRange(numBins, options) {
2306
+ const low = Math.max(0, options?.lowBin ?? 0);
2307
+ const high = Math.min(numBins, options?.highBin ?? numBins);
2308
+ return { low, high };
2309
+ }
2310
+ function reduceMean(input, options) {
2311
+ const nFrames = input.data.length;
2312
+ const values = new Float32Array(nFrames);
2313
+ for (let t = 0; t < nFrames; t++) {
2314
+ const frame = input.data[t];
2315
+ if (!frame || frame.length === 0) {
2316
+ values[t] = 0;
2317
+ continue;
2318
+ }
2319
+ const { low, high } = getBinRange(frame.length, options?.binRange);
2320
+ let sum = 0;
2321
+ const count = high - low;
2322
+ for (let k = low; k < high; k++) {
2323
+ sum += frame[k] ?? 0;
2324
+ }
2325
+ values[t] = count > 0 ? sum / count : 0;
2326
+ }
2327
+ return {
2328
+ times: input.times,
2329
+ values,
2330
+ valueRange: computeValueRange(values)
2331
+ };
2332
+ }
2333
+ function reduceMax(input, options) {
2334
+ const nFrames = input.data.length;
2335
+ const values = new Float32Array(nFrames);
2336
+ for (let t = 0; t < nFrames; t++) {
2337
+ const frame = input.data[t];
2338
+ if (!frame || frame.length === 0) {
2339
+ values[t] = 0;
2340
+ continue;
2341
+ }
2342
+ const { low, high } = getBinRange(frame.length, options?.binRange);
2343
+ let max = -Infinity;
2344
+ for (let k = low; k < high; k++) {
2345
+ const v = frame[k] ?? 0;
2346
+ if (v > max) max = v;
2347
+ }
2348
+ values[t] = max === -Infinity ? 0 : max;
2349
+ }
2350
+ return {
2351
+ times: input.times,
2352
+ values,
2353
+ valueRange: computeValueRange(values)
2354
+ };
2355
+ }
2356
+ function reduceSum(input, options) {
2357
+ const nFrames = input.data.length;
2358
+ const values = new Float32Array(nFrames);
2359
+ for (let t = 0; t < nFrames; t++) {
2360
+ const frame = input.data[t];
2361
+ if (!frame || frame.length === 0) {
2362
+ values[t] = 0;
2363
+ continue;
2364
+ }
2365
+ const { low, high } = getBinRange(frame.length, options?.binRange);
2366
+ let sum = 0;
2367
+ for (let k = low; k < high; k++) {
2368
+ sum += frame[k] ?? 0;
2369
+ }
2370
+ values[t] = sum;
2371
+ }
2372
+ return {
2373
+ times: input.times,
2374
+ values,
2375
+ valueRange: computeValueRange(values)
2376
+ };
2377
+ }
2378
+ function reduceVariance(input, options) {
2379
+ const nFrames = input.data.length;
2380
+ const values = new Float32Array(nFrames);
2381
+ for (let t = 0; t < nFrames; t++) {
2382
+ const frame = input.data[t];
2383
+ if (!frame || frame.length === 0) {
2384
+ values[t] = 0;
2385
+ continue;
2386
+ }
2387
+ const { low, high } = getBinRange(frame.length, options?.binRange);
2388
+ const count = high - low;
2389
+ if (count <= 1) {
2390
+ values[t] = 0;
2391
+ continue;
2392
+ }
2393
+ let sum = 0;
2394
+ for (let k = low; k < high; k++) {
2395
+ sum += frame[k] ?? 0;
2396
+ }
2397
+ const mean = sum / count;
2398
+ let variance = 0;
2399
+ for (let k = low; k < high; k++) {
2400
+ const d = (frame[k] ?? 0) - mean;
2401
+ variance += d * d;
2402
+ }
2403
+ values[t] = variance / count;
2404
+ }
2405
+ return {
2406
+ times: input.times,
2407
+ values,
2408
+ valueRange: computeValueRange(values)
2409
+ };
2410
+ }
2411
+ function reduceAmplitude(input, options) {
2412
+ return reduceSum(input, options);
2413
+ }
2414
+ function reduceSpectralFlux(input, options) {
2415
+ const nFrames = input.data.length;
2416
+ const values = new Float32Array(nFrames);
2417
+ const normalized = options?.spectralFlux?.normalized ?? true;
2418
+ if (nFrames === 0) {
2419
+ return {
2420
+ times: input.times,
2421
+ values,
2422
+ valueRange: { min: 0, max: 0 }
2423
+ };
2424
+ }
2425
+ values[0] = 0;
2426
+ let prevNorm = null;
2427
+ for (let t = 0; t < nFrames; t++) {
2428
+ const frame = input.data[t];
2429
+ if (!frame || frame.length === 0) {
2430
+ values[t] = 0;
2431
+ prevNorm = null;
2432
+ continue;
2433
+ }
2434
+ const { low, high } = getBinRange(frame.length, options?.binRange);
2435
+ let curNorm;
2436
+ if (normalized) {
2437
+ let sum = 0;
2438
+ for (let k = low; k < high; k++) {
2439
+ sum += frame[k] ?? 0;
2440
+ }
2441
+ if (sum <= 0) {
2442
+ values[t] = 0;
2443
+ prevNorm = null;
2444
+ continue;
2445
+ }
2446
+ const inv = 1 / sum;
2447
+ curNorm = new Float32Array(high - low);
2448
+ for (let k = low; k < high; k++) {
2449
+ curNorm[k - low] = (frame[k] ?? 0) * inv;
2450
+ }
2451
+ } else {
2452
+ curNorm = new Float32Array(high - low);
2453
+ for (let k = low; k < high; k++) {
2454
+ curNorm[k - low] = frame[k] ?? 0;
2455
+ }
2456
+ }
2457
+ if (!prevNorm || prevNorm.length !== curNorm.length) {
2458
+ values[t] = 0;
2459
+ prevNorm = curNorm;
2460
+ continue;
2461
+ }
2462
+ let flux = 0;
2463
+ for (let k = 0; k < curNorm.length; k++) {
2464
+ flux += Math.abs((curNorm[k] ?? 0) - (prevNorm[k] ?? 0));
2465
+ }
2466
+ values[t] = flux;
2467
+ prevNorm = curNorm;
2468
+ }
2469
+ return {
2470
+ times: input.times,
2471
+ values,
2472
+ valueRange: computeValueRange(values)
2473
+ };
2474
+ }
2475
+ function reduceSpectralCentroid(input, options) {
2476
+ const nFrames = input.data.length;
2477
+ const values = new Float32Array(nFrames);
2478
+ for (let t = 0; t < nFrames; t++) {
2479
+ const frame = input.data[t];
2480
+ if (!frame || frame.length === 0) {
2481
+ values[t] = 0;
2482
+ continue;
2483
+ }
2484
+ const { low, high } = getBinRange(frame.length, options?.binRange);
2485
+ let num = 0;
2486
+ let den = 0;
2487
+ for (let k = low; k < high; k++) {
2488
+ const m = frame[k] ?? 0;
2489
+ if (m > 0) {
2490
+ num += k * m;
2491
+ den += m;
2492
+ }
2493
+ }
2494
+ values[t] = den > 0 ? num / den : 0;
2495
+ }
2496
+ return {
2497
+ times: input.times,
2498
+ values,
2499
+ valueRange: computeValueRange(values)
2500
+ };
2501
+ }
2502
+ function reduceOnsetStrength(input, options) {
2503
+ const nFrames = input.data.length;
2504
+ const values = new Float32Array(nFrames);
2505
+ const useLog = options?.onsetStrength?.useLog ?? true;
2506
+ const smoothMs = options?.onsetStrength?.smoothMs ?? 10;
2507
+ const diffMethod = options?.onsetStrength?.diffMethod ?? "rectified";
2508
+ if (nFrames === 0) {
2509
+ return {
2510
+ times: input.times,
2511
+ values,
2512
+ valueRange: { min: 0, max: 0 }
2513
+ };
2514
+ }
2515
+ values[0] = 0;
2516
+ for (let t = 1; t < nFrames; t++) {
2517
+ const cur = input.data[t];
2518
+ const prev = input.data[t - 1];
2519
+ if (!cur || !prev || cur.length === 0 || prev.length === 0) {
2520
+ values[t] = 0;
2521
+ continue;
2522
+ }
2523
+ const { low, high } = getBinRange(cur.length, options?.binRange);
2524
+ let sum = 0;
2525
+ let binsWithData = 0;
2526
+ for (let k = low; k < high; k++) {
2527
+ let a = cur[k] ?? 0;
2528
+ let b = prev[k] ?? 0;
2529
+ if (a > 0 || b > 0) {
2530
+ binsWithData++;
2531
+ if (useLog) {
2532
+ a = logCompress2(a);
2533
+ b = logCompress2(b);
2534
+ }
2535
+ const d = a - b;
2536
+ sum += diffMethod === "abs" ? Math.abs(d) : Math.max(0, d);
2537
+ }
2538
+ }
2539
+ values[t] = binsWithData > 0 ? sum / binsWithData : 0;
2540
+ }
2541
+ if (smoothMs > 0 && nFrames >= 2) {
2542
+ const dt = (input.times[1] ?? 0) - (input.times[0] ?? 0);
2543
+ if (dt > 0) {
2544
+ const windowFrames = Math.max(1, Math.round(smoothMs / 1e3 / dt));
2545
+ const smoothed = movingAverage2(values, windowFrames | 1);
2546
+ return {
2547
+ times: input.times,
2548
+ values: smoothed,
2549
+ valueRange: computeValueRange(smoothed)
2550
+ };
2551
+ }
2552
+ }
2553
+ return {
2554
+ times: input.times,
2555
+ values,
2556
+ valueRange: computeValueRange(values)
2557
+ };
2558
+ }
2559
+ function reduce2DToSignal(input, algorithm, options) {
2560
+ switch (algorithm) {
2561
+ case "mean":
2562
+ return reduceMean(input, options);
2563
+ case "max":
2564
+ return reduceMax(input, options);
2565
+ case "sum":
2566
+ return reduceSum(input, options);
2567
+ case "variance":
2568
+ return reduceVariance(input, options);
2569
+ case "amplitude":
2570
+ return reduceAmplitude(input, options);
2571
+ case "spectralFlux":
2572
+ return reduceSpectralFlux(input, options);
2573
+ case "spectralCentroid":
2574
+ return reduceSpectralCentroid(input, options);
2575
+ case "onsetStrength":
2576
+ return reduceOnsetStrength(input, options);
2577
+ default:
2578
+ const _exhaustive = algorithm;
2579
+ throw new Error(`Unknown reduction algorithm: ${_exhaustive}`);
2580
+ }
2581
+ }
2582
+ function getReductionAlgorithmLabel(algorithm) {
2583
+ switch (algorithm) {
2584
+ case "mean":
2585
+ return "Mean";
2586
+ case "max":
2587
+ return "Maximum";
2588
+ case "sum":
2589
+ return "Sum";
2590
+ case "variance":
2591
+ return "Variance";
2592
+ case "amplitude":
2593
+ return "Amplitude Envelope";
2594
+ case "spectralFlux":
2595
+ return "Spectral Flux";
2596
+ case "spectralCentroid":
2597
+ return "Spectral Centroid";
2598
+ case "onsetStrength":
2599
+ return "Onset Strength";
2600
+ default:
2601
+ return String(algorithm);
2602
+ }
2603
+ }
2604
+ function getReductionAlgorithmDescription(algorithm) {
2605
+ switch (algorithm) {
2606
+ case "mean":
2607
+ return "Average value across all bins per frame";
2608
+ case "max":
2609
+ return "Maximum value across all bins per frame";
2610
+ case "sum":
2611
+ return "Sum of all bin values per frame";
2612
+ case "variance":
2613
+ return "Variance of bin values per frame";
2614
+ case "amplitude":
2615
+ return "Sum of magnitudes (energy envelope)";
2616
+ case "spectralFlux":
2617
+ return "Change between consecutive frames";
2618
+ case "spectralCentroid":
2619
+ return "Weighted center frequency";
2620
+ case "onsetStrength":
2621
+ return "Temporal derivative for onset detection";
2622
+ default:
2623
+ return "";
2624
+ }
2625
+ }
2626
+ function applyPolarity(values, mode) {
2627
+ if (mode === "signed") {
2628
+ return values;
2629
+ }
2630
+ const result = new Float32Array(values.length);
2631
+ for (let i = 0; i < values.length; i++) {
2632
+ result[i] = Math.abs(values[i] ?? 0);
2633
+ }
2634
+ return result;
2635
+ }
2636
+ function getStabilizationWindowFrames(mode, frameTime) {
2637
+ const smoothingTimes = {
2638
+ none: 0,
2639
+ light: 0.01,
2640
+ // 10ms
2641
+ medium: 0.03,
2642
+ // 30ms
2643
+ heavy: 0.1
2644
+ // 100ms
2645
+ };
2646
+ const smoothMs = smoothingTimes[mode] * 1e3;
2647
+ if (smoothMs <= 0 || frameTime <= 0) return 1;
2648
+ return Math.max(1, Math.round(smoothMs / 1e3 / frameTime)) | 1;
2649
+ }
2650
+ function applyAttackRelease(values, times, attackTimeSec, releaseTimeSec) {
2651
+ const n = values.length;
2652
+ if (n === 0) return values;
2653
+ const out = new Float32Array(n);
2654
+ out[0] = values[0] ?? 0;
2655
+ for (let i = 1; i < n; i++) {
2656
+ const dt = (times[i] ?? 0) - (times[i - 1] ?? 0);
2657
+ if (dt <= 0) {
2658
+ out[i] = values[i] ?? 0;
2659
+ continue;
2660
+ }
2661
+ const current = values[i] ?? 0;
2662
+ const prev = out[i - 1] ?? 0;
2663
+ if (current > prev) {
2664
+ if (attackTimeSec > 0) {
2665
+ const alpha = 1 - Math.exp(-dt / attackTimeSec);
2666
+ out[i] = prev + alpha * (current - prev);
2667
+ } else {
2668
+ out[i] = current;
2669
+ }
2670
+ } else {
2671
+ if (releaseTimeSec > 0) {
2672
+ const alpha = 1 - Math.exp(-dt / releaseTimeSec);
2673
+ out[i] = prev + alpha * (current - prev);
2674
+ } else {
2675
+ out[i] = current;
2676
+ }
2677
+ }
2678
+ }
2679
+ return out;
2680
+ }
2681
+ function stabilizeSignal(values, times, options) {
2682
+ if (values.length === 0) return values;
2683
+ let result = values;
2684
+ if (options.mode !== "none" && times.length >= 2) {
2685
+ const dt = (times[1] ?? 0) - (times[0] ?? 0);
2686
+ if (dt > 0) {
2687
+ const windowFrames = getStabilizationWindowFrames(options.mode, dt);
2688
+ if (windowFrames > 1) {
2689
+ result = movingAverage2(result, windowFrames);
2690
+ }
2691
+ }
2692
+ }
2693
+ if (options.envelopeMode === "attackRelease") {
2694
+ const attackSec = options.attackTimeSec ?? 0.01;
2695
+ const releaseSec = options.releaseTimeSec ?? 0.1;
2696
+ result = applyAttackRelease(result, times, attackSec, releaseSec);
2697
+ }
2698
+ return result;
2699
+ }
2700
+ function computePercentiles(values, percentiles) {
2701
+ if (values.length === 0) {
2702
+ return Object.fromEntries(percentiles.map((p) => [p, 0]));
2703
+ }
2704
+ const sorted = Float32Array.from(values).sort((a, b) => a - b);
2705
+ const n = sorted.length;
2706
+ const result = {};
2707
+ for (const p of percentiles) {
2708
+ const clamped = Math.max(0, Math.min(100, p));
2709
+ const index = clamped / 100 * (n - 1);
2710
+ const lower = Math.floor(index);
2711
+ const upper = Math.min(lower + 1, n - 1);
2712
+ const frac = index - lower;
2713
+ result[p] = (sorted[lower] ?? 0) * (1 - frac) + (sorted[upper] ?? 0) * frac;
2714
+ }
2715
+ return result;
2716
+ }
2717
+ function computeLocalStats(values, times, startTime, endTime) {
2718
+ const indices = [];
2719
+ for (let i = 0; i < times.length; i++) {
2720
+ const t = times[i] ?? 0;
2721
+ if (t >= startTime && t <= endTime) {
2722
+ indices.push(i);
2723
+ }
2724
+ }
2725
+ if (indices.length === 0) {
2726
+ return { min: 0, max: 0, p5: 0, p95: 0 };
2727
+ }
2728
+ const viewportValues = new Float32Array(indices.length);
2729
+ for (let i = 0; i < indices.length; i++) {
2730
+ viewportValues[i] = values[indices[i] ?? 0] ?? 0;
2731
+ }
2732
+ const range = computeValueRange(viewportValues);
2733
+ const percentiles = computePercentiles(viewportValues, [5, 95]);
2734
+ return {
2735
+ min: range.min,
2736
+ max: range.max,
2737
+ p5: percentiles[5] ?? 0,
2738
+ p95: percentiles[95] ?? 0
2739
+ };
2740
+ }
2741
+
1337
2742
  // src/util/normalise.ts
1338
2743
  function normaliseForWaveform(data, options = {}) {
1339
2744
  const center = options.center ?? false;
@@ -1598,7 +3003,7 @@ function similarityFingerprintV1(a, b, weights = {}) {
1598
3003
  }
1599
3004
 
1600
3005
  // src/search/searchTrackV1.ts
1601
- function nowMs() {
3006
+ function nowMs3() {
1602
3007
  return typeof performance !== "undefined" ? performance.now() : Date.now();
1603
3008
  }
1604
3009
  function clamp01(x) {
@@ -1607,7 +3012,7 @@ function clamp01(x) {
1607
3012
  return x;
1608
3013
  }
1609
3014
  async function searchTrackV1(params) {
1610
- const tStart = nowMs();
3015
+ const tStart = nowMs3();
1611
3016
  const options = params.options ?? {};
1612
3017
  const hopSec = Math.max(5e-3, options.hopSec ?? 0.03);
1613
3018
  const threshold = clamp01(options.threshold ?? 0.75);
@@ -1615,7 +3020,7 @@ async function searchTrackV1(params) {
1615
3020
  const qt1 = Math.max(params.queryRegion.t0, params.queryRegion.t1);
1616
3021
  const windowSec = Math.max(1e-3, qt1 - qt0);
1617
3022
  const minSpacingSec = Math.max(0, options.minCandidateSpacingSec ?? windowSec * 0.8);
1618
- const tFp0 = nowMs();
3023
+ const tFp0 = nowMs3();
1619
3024
  const queryFp = fingerprintV1({
1620
3025
  t0: qt0,
1621
3026
  t1: qt1,
@@ -1624,8 +3029,8 @@ async function searchTrackV1(params) {
1624
3029
  mfcc: params.mfcc,
1625
3030
  peakPick: options.queryPeakPick
1626
3031
  });
1627
- const fingerprintMs = nowMs() - tFp0;
1628
- const scanStartMs = nowMs();
3032
+ const fingerprintMs = nowMs3() - tFp0;
3033
+ const scanStartMs = nowMs3();
1629
3034
  const trackDuration = Math.max(
1630
3035
  params.mel.times.length ? params.mel.times[params.mel.times.length - 1] ?? 0 : 0,
1631
3036
  params.onsetEnvelope.times.length ? params.onsetEnvelope.times[params.onsetEnvelope.times.length - 1] ?? 0 : 0
@@ -1665,7 +3070,7 @@ async function searchTrackV1(params) {
1665
3070
  sim[w] = clamp01(score);
1666
3071
  scannedWindows++;
1667
3072
  }
1668
- const scanMs = nowMs() - scanStartMs;
3073
+ const scanMs = nowMs3() - scanStartMs;
1669
3074
  const events = peakPick(times, sim, {
1670
3075
  threshold,
1671
3076
  minIntervalSec: minSpacingSec,
@@ -1681,7 +3086,7 @@ async function searchTrackV1(params) {
1681
3086
  windowEndSec
1682
3087
  };
1683
3088
  });
1684
- const totalMs = nowMs() - tStart;
3089
+ const totalMs = nowMs3() - tStart;
1685
3090
  return {
1686
3091
  times,
1687
3092
  similarity: sim,
@@ -1915,7 +3320,7 @@ function scoreWithModelV1(model, x) {
1915
3320
  }
1916
3321
 
1917
3322
  // src/search/searchTrackV1Guided.ts
1918
- function nowMs2() {
3323
+ function nowMs4() {
1919
3324
  return typeof performance !== "undefined" ? performance.now() : Date.now();
1920
3325
  }
1921
3326
  function clamp013(x) {
@@ -2082,7 +3487,7 @@ var SlidingMoments = class {
2082
3487
  }
2083
3488
  };
2084
3489
  async function searchTrackV1Guided(params) {
2085
- const tStart = nowMs2();
3490
+ const tStart = nowMs4();
2086
3491
  const options = params.options ?? {};
2087
3492
  const hopSec = Math.max(5e-3, options.hopSec ?? 0.03);
2088
3493
  const threshold = clamp013(options.threshold ?? 0.75);
@@ -2101,7 +3506,7 @@ async function searchTrackV1Guided(params) {
2101
3506
  positives: modelDecision.positives.length,
2102
3507
  negatives: modelDecision.negatives.length
2103
3508
  } : { kind: "baseline", positives: 0, negatives: 0 };
2104
- const tPrep0 = nowMs2();
3509
+ const tPrep0 = nowMs4();
2105
3510
  const timesFrames = params.mel.times;
2106
3511
  const nFrames = timesFrames.length;
2107
3512
  const trackDuration = Math.max(
@@ -2112,7 +3517,7 @@ async function searchTrackV1Guided(params) {
2112
3517
  const times = new Float32Array(nWindows);
2113
3518
  const scores = new Float32Array(nWindows);
2114
3519
  if (nWindows === 0) {
2115
- const totalMs2 = nowMs2() - tStart;
3520
+ const totalMs2 = nowMs4() - tStart;
2116
3521
  return {
2117
3522
  times,
2118
3523
  scores,
@@ -2201,7 +3606,7 @@ async function searchTrackV1Guided(params) {
2201
3606
  }
2202
3607
  const peakPrefix = new Uint32Array(nFrames + 1);
2203
3608
  for (let i = 0; i < nFrames; i++) peakPrefix[i + 1] = (peakPrefix[i] ?? 0) + (isPeak[i] ?? 0);
2204
- const fingerprintMs = nowMs2() - tPrep0;
3609
+ const fingerprintMs = nowMs4() - tPrep0;
2205
3610
  const addMelFrame = (frame, sum, sumSq) => {
2206
3611
  const row = melBands[frame];
2207
3612
  const s = melScale[frame] ?? 1;
@@ -2387,7 +3792,7 @@ async function searchTrackV1Guided(params) {
2387
3792
  };
2388
3793
  let skippedWindows = 0;
2389
3794
  let scannedWindows = 0;
2390
- const scanStartMs = nowMs2();
3795
+ const scanStartMs = nowMs4();
2391
3796
  let curveKind = "similarity";
2392
3797
  let modelExplain = baselineExplain;
2393
3798
  let modelMs = 0;
@@ -2410,7 +3815,7 @@ async function searchTrackV1Guided(params) {
2410
3815
  if (modelDecision.kind === "baseline") {
2411
3816
  runBaselineSimilarityScan();
2412
3817
  } else {
2413
- const tModel0 = nowMs2();
3818
+ const tModel0 = nowMs4();
2414
3819
  curveKind = "confidence";
2415
3820
  try {
2416
3821
  const dim = layout.dim;
@@ -2475,10 +3880,10 @@ async function searchTrackV1Guided(params) {
2475
3880
  zInvStd = null;
2476
3881
  runBaselineSimilarityScan();
2477
3882
  } finally {
2478
- modelMs = nowMs2() - tModel0;
3883
+ modelMs = nowMs4() - tModel0;
2479
3884
  }
2480
3885
  }
2481
- const scanMs = nowMs2() - scanStartMs;
3886
+ const scanMs = nowMs4() - scanStartMs;
2482
3887
  const events = peakPick(times, scores, {
2483
3888
  threshold,
2484
3889
  minIntervalSec: minSpacingSec,
@@ -2505,7 +3910,7 @@ async function searchTrackV1Guided(params) {
2505
3910
  };
2506
3911
  }
2507
3912
  }
2508
- const totalMs = nowMs2() - tStart;
3913
+ const totalMs = nowMs4() - tStart;
2509
3914
  return {
2510
3915
  times,
2511
3916
  scores,
@@ -2531,6 +3936,6 @@ function helloMir(name = "world") {
2531
3936
  return `Hello, ${name} from @octoseq/mir v${MIR_VERSION}`;
2532
3937
  }
2533
3938
 
2534
- export { MIR_VERSION, MirGPU, addBandToStructure, allFrequencyBoundsAt, applyBandMaskToSpectrogram, bandAmplitudeEnvelope, bandOnsetStrength, bandSpectralFlux, bandsActiveAt, binToHz, clampDb, computeBandMaskAtTime, computeBeatPosition, computeBeatPositionFromStructure, computeFrameAmplitude, computeFrameEnergy, computePhaseHypotheses, createBandStructure, createConstantBand, createMusicalTimeStructure, createSectionedBand, createSegmentFromGrid, createStandardBands, findBandById, findSegmentAtTime, fingerprintToVectorV1, fingerprintV1, frequencyBoundsAt, generateBandId, generateBandProposals, generateBeatTimes, generateSegmentBeatTimes, generateSegmentId, getBandMirFunctionLabel, helloMir, hzToBin, keyframesFromBand, mergeAdjacentSegments, minMax, moveKeyframeTime, normaliseForWaveform, removeBandFromStructure, removeKeyframe, runBandMirBatch, searchTrackV1, searchTrackV1Guided, segmentsFromKeyframes, similarityFingerprintV1, sortBands, sortFrequencySegments, sortSegments, spectrogramToDb, splitBandSegmentAt, splitSegment, touchStructure, updateBandInStructure, updateKeyframe, validateBandStructure, validateFrequencyBand, validateFrequencySegments, validateSegments };
3939
+ export { DEFAULT_PEAK_PICKING_PARAMS, MIR_VERSION, MirGPU, addBandToStructure, allFrequencyBoundsAt, applyBandMaskToCqt, applyBandMaskToSpectrogram, applyHysteresisGate, applyPolarity, bandAmplitudeEnvelope, bandBeatCandidates, bandCqtBassPitchMotion, bandCqtHarmonicEnergy, bandCqtTonalStability, bandOnsetPeaks, bandOnsetStrength, bandSpectralCentroid, bandSpectralFlux, bandsActiveAt, binToHz, clampDb, computeAdaptiveThreshold, computeBandMaskAtTime, computeBeatPosition, computeBeatPositionFromStructure, computeFrameAmplitude, computeFrameEnergy, computeLocalStats, computePercentiles, computePhaseHypotheses, createBandStructure, createConstantBand, createMusicalTimeStructure, createSectionedBand, createSegmentFromGrid, createStandardBands, findBandById, findSegmentAtTime, fingerprintToVectorV1, fingerprintV1, frequencyBoundsAt, generateBandId, generateBandProposals, generateBeatTimes, generateSegmentBeatTimes, generateSegmentId, getBandCqtFunctionLabel, getBandEventFunctionLabel, getBandMirFunctionLabel, getReductionAlgorithmDescription, getReductionAlgorithmLabel, helloMir, hzToBin, keyframesFromBand, mergeAdjacentSegments, minMax, moveKeyframeTime, normaliseForWaveform, pickPeaks, pickPeaksAdaptive, reduce2DToSignal, removeBandFromStructure, removeKeyframe, runBandCqtBatch, runBandEventsBatch, runBandMirBatch, searchTrackV1, searchTrackV1Guided, segmentsFromKeyframes, similarityFingerprintV1, sortBands, sortFrequencySegments, sortSegments, spectrogramToDb, splitBandSegmentAt, splitSegment, stabilizeSignal, touchStructure, updateBandInStructure, updateKeyframe, validateBandStructure, validateFrequencyBand, validateFrequencySegments, validateSegments };
2535
3940
  //# sourceMappingURL=index.js.map
2536
3941
  //# sourceMappingURL=index.js.map