@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/{chunk-OLIDGECY.js → chunk-CI7QGWP7.js} +44 -3
- package/dist/chunk-CI7QGWP7.js.map +1 -0
- package/dist/index.d.ts +475 -7
- package/dist/index.js +1495 -90
- package/dist/index.js.map +1 -1
- package/dist/{runMir-CWsxri61.d.ts → runMir-D4t7WsN0.d.ts} +1 -1
- package/dist/runner/runMir.d.ts +2 -2
- package/dist/runner/runMir.js +1 -1
- package/dist/runner/workerProtocol.d.ts +1 -1
- package/dist/{types-D6eBRofe.d.ts → types-ifqndzu7.d.ts} +86 -6
- package/package.json +1 -1
- package/src/dsp/bandCqt.ts +662 -0
- package/src/dsp/bandEvents.ts +351 -0
- package/src/dsp/bandMir.ts +69 -0
- package/src/dsp/bandProposal.ts +30 -2
- package/src/dsp/customSignalReduction.ts +841 -0
- package/src/dsp/frequencyBand.ts +46 -3
- package/src/dsp/peakPicking.ts +519 -0
- package/src/dsp/spectral.ts +54 -0
- package/src/index.ts +93 -1
- package/src/runner/runMir.ts +24 -1
- package/src/types.ts +97 -4
- package/dist/chunk-OLIDGECY.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { withCqtDefaults, cqtSpectrogram, harmonicEnergy, bassPitchMotion, tonalStability,
|
|
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-
|
|
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:
|
|
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/
|
|
1046
|
-
|
|
1047
|
-
|
|
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
|
|
1057
|
-
|
|
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
|
|
1060
|
-
const
|
|
1061
|
-
const
|
|
1062
|
-
const
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
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
|
-
|
|
1071
|
-
|
|
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
|
-
|
|
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
|
|
1076
|
-
const
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
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 (
|
|
1088
|
-
|
|
1555
|
+
if (bandEventResults.length > 0) {
|
|
1556
|
+
results.set(bandId, bandEventResults);
|
|
1089
1557
|
}
|
|
1090
1558
|
}
|
|
1091
|
-
|
|
1559
|
+
const endMs = nowMs();
|
|
1560
|
+
return {
|
|
1561
|
+
results,
|
|
1562
|
+
totalTimingMs: endMs - startMs
|
|
1563
|
+
};
|
|
1092
1564
|
}
|
|
1093
|
-
function
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
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
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
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
|
|
1105
|
-
const
|
|
1106
|
-
|
|
1107
|
-
|
|
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
|
|
1110
|
-
const
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
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
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1629
|
+
const first = band.frequencyShape[0];
|
|
1630
|
+
if (first) {
|
|
1631
|
+
return { lowHz: first.lowHzStart, highHz: first.highHzStart };
|
|
1125
1632
|
}
|
|
1126
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
1628
|
-
const scanStartMs =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
3883
|
+
modelMs = nowMs4() - tModel0;
|
|
2479
3884
|
}
|
|
2480
3885
|
}
|
|
2481
|
-
const scanMs =
|
|
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 =
|
|
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
|