@waveform-playlist/spectrogram 9.5.0 → 9.5.1
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.d.mts +16 -25
- package/dist/index.d.ts +16 -25
- package/dist/index.js +356 -266
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +354 -266
- package/dist/index.mjs.map +1 -1
- package/dist/worker/spectrogram.worker.mjs +190 -159
- package/dist/worker/spectrogram.worker.mjs.map +1 -1
- package/package.json +4 -4
|
@@ -150,12 +150,41 @@ function getFrequencyScale(name) {
|
|
|
150
150
|
// src/worker/spectrogram.worker.ts
|
|
151
151
|
var canvasRegistry = /* @__PURE__ */ new Map();
|
|
152
152
|
var audioDataRegistry = /* @__PURE__ */ new Map();
|
|
153
|
+
var MAX_CACHE_ENTRIES = 16;
|
|
153
154
|
var fftCache = /* @__PURE__ */ new Map();
|
|
155
|
+
function evictLRUCacheEntries(keepKey) {
|
|
156
|
+
while (fftCache.size >= MAX_CACHE_ENTRIES) {
|
|
157
|
+
let deleted = false;
|
|
158
|
+
for (const key of fftCache.keys()) {
|
|
159
|
+
if (key !== keepKey) {
|
|
160
|
+
fftCache.delete(key);
|
|
161
|
+
deleted = true;
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (!deleted) break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function touchCacheEntry(key) {
|
|
169
|
+
const entry = fftCache.get(key);
|
|
170
|
+
if (entry) {
|
|
171
|
+
fftCache.delete(key);
|
|
172
|
+
fftCache.set(key, entry);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
154
175
|
function generateCacheKey(params) {
|
|
155
176
|
const { compute: c } = params;
|
|
156
177
|
return `${params.clipId}:${params.channelIndex}:${params.offsetSamples}:${params.durationSamples}:${params.sampleRate}:${c.fftSize ?? ""}:${c.zeroPaddingFactor ?? ""}:${c.hopSize ?? ""}:${c.windowFunction ?? ""}:${c.alpha ?? ""}:${params.mono ? 1 : 0}`;
|
|
157
178
|
}
|
|
158
|
-
|
|
179
|
+
var latestGeneration = 0;
|
|
180
|
+
var FRAMES_PER_YIELD = 2e3;
|
|
181
|
+
function isGenerationStale(generation) {
|
|
182
|
+
return generation < latestGeneration;
|
|
183
|
+
}
|
|
184
|
+
function yieldToMessageQueue() {
|
|
185
|
+
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
186
|
+
}
|
|
187
|
+
async function computeFromChannelData(channelData, config, sampleRate, offsetSamples, durationSamples, generation) {
|
|
159
188
|
const windowSize = config.fftSize ?? 2048;
|
|
160
189
|
const zeroPaddingFactor = config.zeroPaddingFactor ?? 2;
|
|
161
190
|
const actualFftSize = windowSize * zeroPaddingFactor;
|
|
@@ -172,6 +201,10 @@ function computeFromChannelData(channelData, config, sampleRate, offsetSamples,
|
|
|
172
201
|
const real = new Float32Array(actualFftSize);
|
|
173
202
|
const dbBuf = new Float32Array(frequencyBinCount);
|
|
174
203
|
for (let frame = 0; frame < frameCount; frame++) {
|
|
204
|
+
if (frame > 0 && frame % FRAMES_PER_YIELD === 0) {
|
|
205
|
+
await yieldToMessageQueue();
|
|
206
|
+
if (isGenerationStale(generation)) return null;
|
|
207
|
+
}
|
|
175
208
|
const start = offsetSamples + frame * hopSize;
|
|
176
209
|
for (let i = 0; i < windowSize; i++) {
|
|
177
210
|
const sampleIdx = start + i;
|
|
@@ -195,9 +228,16 @@ function computeFromChannelData(channelData, config, sampleRate, offsetSamples,
|
|
|
195
228
|
rangeDb
|
|
196
229
|
};
|
|
197
230
|
}
|
|
198
|
-
function computeMonoFromChannels(channels, config, sampleRate, offsetSamples, durationSamples) {
|
|
231
|
+
async function computeMonoFromChannels(channels, config, sampleRate, offsetSamples, durationSamples, generation) {
|
|
199
232
|
if (channels.length === 1) {
|
|
200
|
-
return computeFromChannelData(
|
|
233
|
+
return computeFromChannelData(
|
|
234
|
+
channels[0],
|
|
235
|
+
config,
|
|
236
|
+
sampleRate,
|
|
237
|
+
offsetSamples,
|
|
238
|
+
durationSamples,
|
|
239
|
+
generation
|
|
240
|
+
);
|
|
201
241
|
}
|
|
202
242
|
const windowSize = config.fftSize ?? 2048;
|
|
203
243
|
const zeroPaddingFactor = config.zeroPaddingFactor ?? 2;
|
|
@@ -215,6 +255,10 @@ function computeMonoFromChannels(channels, config, sampleRate, offsetSamples, du
|
|
|
215
255
|
const real = new Float32Array(actualFftSize);
|
|
216
256
|
const dbBuf = new Float32Array(frequencyBinCount);
|
|
217
257
|
for (let frame = 0; frame < frameCount; frame++) {
|
|
258
|
+
if (frame > 0 && frame % FRAMES_PER_YIELD === 0) {
|
|
259
|
+
await yieldToMessageQueue();
|
|
260
|
+
if (isGenerationStale(generation)) return null;
|
|
261
|
+
}
|
|
218
262
|
const start = offsetSamples + frame * hopSize;
|
|
219
263
|
for (let i = 0; i < windowSize; i++) {
|
|
220
264
|
const sampleIdx = start + i;
|
|
@@ -313,7 +357,12 @@ function renderSpectrogramToCanvas(specData, canvasIds, canvasWidths, canvasHeig
|
|
|
313
357
|
} else {
|
|
314
358
|
const tmpCanvas = new OffscreenCanvas(canvasWidth, canvasHeight);
|
|
315
359
|
const tmpCtx = tmpCanvas.getContext("2d");
|
|
316
|
-
if (!tmpCtx)
|
|
360
|
+
if (!tmpCtx) {
|
|
361
|
+
console.warn(
|
|
362
|
+
`[spectrogram-worker] getContext('2d') failed for DPR scaling of "${canvasIds[chunkIdx]}"`
|
|
363
|
+
);
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
317
366
|
tmpCtx.putImageData(imgData, 0, 0);
|
|
318
367
|
ctx.imageSmoothingEnabled = false;
|
|
319
368
|
ctx.drawImage(tmpCanvas, 0, 0, offscreen.width, offscreen.height);
|
|
@@ -321,6 +370,105 @@ function renderSpectrogramToCanvas(specData, canvasIds, canvasWidths, canvasHeig
|
|
|
321
370
|
if (!globalPixelOffsets) accumulatedOffset += canvasWidth;
|
|
322
371
|
}
|
|
323
372
|
}
|
|
373
|
+
async function handleComputeFFT(msg) {
|
|
374
|
+
const {
|
|
375
|
+
id,
|
|
376
|
+
generation,
|
|
377
|
+
clipId,
|
|
378
|
+
config,
|
|
379
|
+
sampleRate: msgSampleRate,
|
|
380
|
+
offsetSamples,
|
|
381
|
+
durationSamples,
|
|
382
|
+
mono,
|
|
383
|
+
sampleRange,
|
|
384
|
+
channelFilter
|
|
385
|
+
} = msg;
|
|
386
|
+
const registered = audioDataRegistry.get(clipId);
|
|
387
|
+
const channelDataArrays = registered && msg.channelDataArrays.length === 0 ? registered.channelDataArrays : msg.channelDataArrays;
|
|
388
|
+
const sampleRate = registered && msg.channelDataArrays.length === 0 ? registered.sampleRate : msgSampleRate;
|
|
389
|
+
const fftSize = config.fftSize ?? 2048;
|
|
390
|
+
const zeroPaddingFactor = config.zeroPaddingFactor ?? 2;
|
|
391
|
+
const hopSize = config.hopSize ?? Math.floor(fftSize / 4);
|
|
392
|
+
const windowFunction = config.windowFunction ?? "hann";
|
|
393
|
+
const effectiveOffset = sampleRange ? sampleRange.start : offsetSamples;
|
|
394
|
+
const effectiveDuration = sampleRange ? sampleRange.end - sampleRange.start : durationSamples;
|
|
395
|
+
const cacheKey = generateCacheKey({
|
|
396
|
+
clipId,
|
|
397
|
+
channelIndex: 0,
|
|
398
|
+
offsetSamples: effectiveOffset,
|
|
399
|
+
durationSamples: effectiveDuration,
|
|
400
|
+
sampleRate,
|
|
401
|
+
compute: { fftSize, zeroPaddingFactor, hopSize, windowFunction, alpha: config.alpha },
|
|
402
|
+
mono
|
|
403
|
+
});
|
|
404
|
+
if (!fftCache.has(cacheKey)) {
|
|
405
|
+
evictLRUCacheEntries(cacheKey);
|
|
406
|
+
const spectrograms = [];
|
|
407
|
+
if (mono || channelDataArrays.length === 1) {
|
|
408
|
+
const result = await computeMonoFromChannels(
|
|
409
|
+
channelDataArrays,
|
|
410
|
+
config,
|
|
411
|
+
sampleRate,
|
|
412
|
+
effectiveOffset,
|
|
413
|
+
effectiveDuration,
|
|
414
|
+
generation
|
|
415
|
+
);
|
|
416
|
+
if (result === null) {
|
|
417
|
+
const response2 = { id, type: "aborted" };
|
|
418
|
+
self.postMessage(response2);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
spectrograms.push(result);
|
|
422
|
+
} else if (channelFilter !== void 0) {
|
|
423
|
+
const channelData = channelDataArrays[channelFilter];
|
|
424
|
+
if (!channelData) {
|
|
425
|
+
const response2 = {
|
|
426
|
+
id,
|
|
427
|
+
type: "error",
|
|
428
|
+
error: `channelFilter ${channelFilter} out of range (${channelDataArrays.length} channels)`
|
|
429
|
+
};
|
|
430
|
+
self.postMessage(response2);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
const result = await computeFromChannelData(
|
|
434
|
+
channelData,
|
|
435
|
+
config,
|
|
436
|
+
sampleRate,
|
|
437
|
+
effectiveOffset,
|
|
438
|
+
effectiveDuration,
|
|
439
|
+
generation
|
|
440
|
+
);
|
|
441
|
+
if (result === null) {
|
|
442
|
+
const response2 = { id, type: "aborted" };
|
|
443
|
+
self.postMessage(response2);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
spectrograms.push(result);
|
|
447
|
+
} else {
|
|
448
|
+
for (const channelData of channelDataArrays) {
|
|
449
|
+
const result = await computeFromChannelData(
|
|
450
|
+
channelData,
|
|
451
|
+
config,
|
|
452
|
+
sampleRate,
|
|
453
|
+
effectiveOffset,
|
|
454
|
+
effectiveDuration,
|
|
455
|
+
generation
|
|
456
|
+
);
|
|
457
|
+
if (result === null) {
|
|
458
|
+
const response2 = { id, type: "aborted" };
|
|
459
|
+
self.postMessage(response2);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
spectrograms.push(result);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
fftCache.set(cacheKey, { spectrograms, sampleOffset: effectiveOffset });
|
|
466
|
+
} else {
|
|
467
|
+
touchCacheEntry(cacheKey);
|
|
468
|
+
}
|
|
469
|
+
const response = { id, type: "cache-key", cacheKey };
|
|
470
|
+
self.postMessage(response);
|
|
471
|
+
}
|
|
324
472
|
self.onmessage = (e) => {
|
|
325
473
|
const msg = e.data;
|
|
326
474
|
if (msg.type === "register-canvas") {
|
|
@@ -364,79 +512,32 @@ self.onmessage = (e) => {
|
|
|
364
512
|
}
|
|
365
513
|
return;
|
|
366
514
|
}
|
|
515
|
+
if (msg.type === "abort-generation") {
|
|
516
|
+
latestGeneration = Math.max(latestGeneration, msg.generation);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
367
519
|
if (msg.type === "compute-fft") {
|
|
368
|
-
const { id
|
|
369
|
-
|
|
370
|
-
const {
|
|
371
|
-
clipId,
|
|
372
|
-
config: config2,
|
|
373
|
-
sampleRate: msgSampleRate,
|
|
374
|
-
offsetSamples: offsetSamples2,
|
|
375
|
-
durationSamples: durationSamples2,
|
|
376
|
-
mono: mono2,
|
|
377
|
-
sampleRange
|
|
378
|
-
} = msg;
|
|
379
|
-
const registered = audioDataRegistry.get(clipId);
|
|
380
|
-
const channelDataArrays2 = registered && msg.channelDataArrays.length === 0 ? registered.channelDataArrays : msg.channelDataArrays;
|
|
381
|
-
const sampleRate2 = registered && msg.channelDataArrays.length === 0 ? registered.sampleRate : msgSampleRate;
|
|
382
|
-
const fftSize = config2.fftSize ?? 2048;
|
|
383
|
-
const zeroPaddingFactor = config2.zeroPaddingFactor ?? 2;
|
|
384
|
-
const hopSize = config2.hopSize ?? Math.floor(fftSize / 4);
|
|
385
|
-
const windowFunction = config2.windowFunction ?? "hann";
|
|
386
|
-
const effectiveOffset = sampleRange ? sampleRange.start : offsetSamples2;
|
|
387
|
-
const effectiveDuration = sampleRange ? sampleRange.end - sampleRange.start : durationSamples2;
|
|
388
|
-
const cacheKey = generateCacheKey({
|
|
389
|
-
clipId,
|
|
390
|
-
channelIndex: 0,
|
|
391
|
-
offsetSamples: effectiveOffset,
|
|
392
|
-
durationSamples: effectiveDuration,
|
|
393
|
-
sampleRate: sampleRate2,
|
|
394
|
-
compute: { fftSize, zeroPaddingFactor, hopSize, windowFunction, alpha: config2.alpha },
|
|
395
|
-
mono: mono2
|
|
396
|
-
});
|
|
397
|
-
if (!fftCache.has(cacheKey)) {
|
|
398
|
-
const clipPrefix = `${clipId}:`;
|
|
399
|
-
for (const key of fftCache.keys()) {
|
|
400
|
-
if (key.startsWith(clipPrefix) && key !== cacheKey) {
|
|
401
|
-
fftCache.delete(key);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
const spectrograms = [];
|
|
405
|
-
if (mono2 || channelDataArrays2.length === 1) {
|
|
406
|
-
spectrograms.push(
|
|
407
|
-
computeMonoFromChannels(
|
|
408
|
-
channelDataArrays2,
|
|
409
|
-
config2,
|
|
410
|
-
sampleRate2,
|
|
411
|
-
effectiveOffset,
|
|
412
|
-
effectiveDuration
|
|
413
|
-
)
|
|
414
|
-
);
|
|
415
|
-
} else {
|
|
416
|
-
for (const channelData of channelDataArrays2) {
|
|
417
|
-
spectrograms.push(
|
|
418
|
-
computeFromChannelData(
|
|
419
|
-
channelData,
|
|
420
|
-
config2,
|
|
421
|
-
sampleRate2,
|
|
422
|
-
effectiveOffset,
|
|
423
|
-
effectiveDuration
|
|
424
|
-
)
|
|
425
|
-
);
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
fftCache.set(cacheKey, { spectrograms, sampleOffset: effectiveOffset });
|
|
429
|
-
}
|
|
430
|
-
const response = { id: id2, type: "cache-key", cacheKey };
|
|
431
|
-
self.postMessage(response);
|
|
432
|
-
} catch (err) {
|
|
433
|
-
const response = { id: id2, type: "error", error: String(err) };
|
|
520
|
+
const { id, generation } = msg;
|
|
521
|
+
if (isGenerationStale(generation)) {
|
|
522
|
+
const response = { id, type: "aborted" };
|
|
434
523
|
self.postMessage(response);
|
|
524
|
+
return;
|
|
435
525
|
}
|
|
526
|
+
handleComputeFFT(msg).catch((err) => {
|
|
527
|
+
const errorMsg = err instanceof Error ? `${err.message}
|
|
528
|
+
${err.stack}` : String(err);
|
|
529
|
+
const response = { id, type: "error", error: errorMsg };
|
|
530
|
+
self.postMessage(response);
|
|
531
|
+
});
|
|
436
532
|
return;
|
|
437
533
|
}
|
|
438
534
|
if (msg.type === "render-chunks") {
|
|
439
|
-
const { id
|
|
535
|
+
const { id, generation } = msg;
|
|
536
|
+
if (isGenerationStale(generation)) {
|
|
537
|
+
const response = { id, type: "aborted" };
|
|
538
|
+
self.postMessage(response);
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
440
541
|
try {
|
|
441
542
|
const {
|
|
442
543
|
cacheKey,
|
|
@@ -455,8 +556,21 @@ self.onmessage = (e) => {
|
|
|
455
556
|
channelIndex
|
|
456
557
|
} = msg;
|
|
457
558
|
const cacheEntry = fftCache.get(cacheKey);
|
|
458
|
-
if (!cacheEntry
|
|
459
|
-
const response2 = {
|
|
559
|
+
if (!cacheEntry) {
|
|
560
|
+
const response2 = {
|
|
561
|
+
id,
|
|
562
|
+
type: "error",
|
|
563
|
+
error: `cache-miss: key "${cacheKey}" not found (cache has ${fftCache.size} entries)`
|
|
564
|
+
};
|
|
565
|
+
self.postMessage(response2);
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
if (channelIndex >= cacheEntry.spectrograms.length) {
|
|
569
|
+
const response2 = {
|
|
570
|
+
id,
|
|
571
|
+
type: "error",
|
|
572
|
+
error: `cache-miss: channelIndex ${channelIndex} out of range (${cacheEntry.spectrograms.length} channels cached)`
|
|
573
|
+
};
|
|
460
574
|
self.postMessage(response2);
|
|
461
575
|
return;
|
|
462
576
|
}
|
|
@@ -479,97 +593,14 @@ self.onmessage = (e) => {
|
|
|
479
593
|
rangeDb,
|
|
480
594
|
cacheEntry.sampleOffset
|
|
481
595
|
);
|
|
482
|
-
const response = { id
|
|
483
|
-
self.postMessage(response);
|
|
484
|
-
} catch (err) {
|
|
485
|
-
const response = { id: id2, type: "error", error: String(err) };
|
|
486
|
-
self.postMessage(response);
|
|
487
|
-
}
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
490
|
-
if (msg.type === "compute-render") {
|
|
491
|
-
const { id: id2 } = msg;
|
|
492
|
-
try {
|
|
493
|
-
const {
|
|
494
|
-
channelDataArrays: channelDataArrays2,
|
|
495
|
-
config: config2,
|
|
496
|
-
sampleRate: sampleRate2,
|
|
497
|
-
offsetSamples: offsetSamples2,
|
|
498
|
-
durationSamples: durationSamples2,
|
|
499
|
-
mono: mono2,
|
|
500
|
-
render
|
|
501
|
-
} = msg;
|
|
502
|
-
const spectrograms = [];
|
|
503
|
-
if (mono2 || channelDataArrays2.length === 1) {
|
|
504
|
-
spectrograms.push(
|
|
505
|
-
computeMonoFromChannels(
|
|
506
|
-
channelDataArrays2,
|
|
507
|
-
config2,
|
|
508
|
-
sampleRate2,
|
|
509
|
-
offsetSamples2,
|
|
510
|
-
durationSamples2
|
|
511
|
-
)
|
|
512
|
-
);
|
|
513
|
-
} else {
|
|
514
|
-
for (const channelData of channelDataArrays2) {
|
|
515
|
-
spectrograms.push(
|
|
516
|
-
computeFromChannelData(channelData, config2, sampleRate2, offsetSamples2, durationSamples2)
|
|
517
|
-
);
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
const scaleFn = getFrequencyScale(render.frequencyScale ?? "mel");
|
|
521
|
-
const isNonLinear = render.frequencyScale !== "linear";
|
|
522
|
-
for (let ch = 0; ch < spectrograms.length; ch++) {
|
|
523
|
-
const channelCanvasIds = render.canvasIds[ch];
|
|
524
|
-
if (!channelCanvasIds || channelCanvasIds.length === 0) continue;
|
|
525
|
-
renderSpectrogramToCanvas(
|
|
526
|
-
spectrograms[ch],
|
|
527
|
-
channelCanvasIds,
|
|
528
|
-
render.canvasWidths,
|
|
529
|
-
render.canvasHeight,
|
|
530
|
-
render.devicePixelRatio,
|
|
531
|
-
render.samplesPerPixel,
|
|
532
|
-
render.colorLUT,
|
|
533
|
-
scaleFn,
|
|
534
|
-
render.minFrequency,
|
|
535
|
-
render.maxFrequency,
|
|
536
|
-
isNonLinear
|
|
537
|
-
);
|
|
538
|
-
}
|
|
539
|
-
const response = { id: id2, type: "done" };
|
|
596
|
+
const response = { id, type: "done" };
|
|
540
597
|
self.postMessage(response);
|
|
541
598
|
} catch (err) {
|
|
542
|
-
const response = { id
|
|
599
|
+
const response = { id, type: "error", error: String(err) };
|
|
543
600
|
self.postMessage(response);
|
|
544
601
|
}
|
|
545
602
|
return;
|
|
546
603
|
}
|
|
547
|
-
|
|
548
|
-
try {
|
|
549
|
-
const spectrograms = [];
|
|
550
|
-
if (mono || channelDataArrays.length === 1) {
|
|
551
|
-
spectrograms.push(
|
|
552
|
-
computeMonoFromChannels(
|
|
553
|
-
channelDataArrays,
|
|
554
|
-
config,
|
|
555
|
-
sampleRate,
|
|
556
|
-
offsetSamples,
|
|
557
|
-
durationSamples
|
|
558
|
-
)
|
|
559
|
-
);
|
|
560
|
-
} else {
|
|
561
|
-
for (const channelData of channelDataArrays) {
|
|
562
|
-
spectrograms.push(
|
|
563
|
-
computeFromChannelData(channelData, config, sampleRate, offsetSamples, durationSamples)
|
|
564
|
-
);
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
const transferables = spectrograms.map((s) => s.data.buffer);
|
|
568
|
-
const response = { id, type: "spectrograms", spectrograms };
|
|
569
|
-
self.postMessage(response, transferables);
|
|
570
|
-
} catch (err) {
|
|
571
|
-
const response = { id, type: "error", error: String(err) };
|
|
572
|
-
self.postMessage(response);
|
|
573
|
-
}
|
|
604
|
+
console.warn(`[spectrogram-worker] Unknown message type: ${msg.type}`);
|
|
574
605
|
};
|
|
575
606
|
//# sourceMappingURL=spectrogram.worker.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/computation/fft.ts","../../src/computation/windowFunctions.ts","../../src/computation/frequencyScales.ts","../../src/worker/spectrogram.worker.ts"],"sourcesContent":["import FFT from 'fft.js';\n\n/**\n * Cache fft.js instances per size (pre-computes twiddle factors).\n */\nconst fftInstances = new Map<number, FFT>();\nconst complexBuffers = new Map<number, number[]>();\n\nfunction getFftInstance(size: number): FFT {\n let instance = fftInstances.get(size);\n if (!instance) {\n instance = new FFT(size);\n fftInstances.set(size, instance);\n complexBuffers.set(size, instance.createComplexArray());\n }\n return instance;\n}\n\nfunction getComplexBuffer(size: number): number[] {\n const buffer = complexBuffers.get(size);\n if (!buffer) {\n throw new Error(`No complex buffer for size ${size}. Call getFftInstance first.`);\n }\n return buffer;\n}\n\n/**\n * In-place FFT using fft.js (radix-4).\n * @param real - Real part (modified in place)\n * @param imag - Imaginary part (modified in place)\n */\nexport function fft(real: Float32Array, imag: Float32Array): void {\n const n = real.length;\n const f = getFftInstance(n);\n const input = f.createComplexArray();\n const out = getComplexBuffer(n);\n\n for (let i = 0; i < n; i++) {\n input[i * 2] = real[i];\n input[i * 2 + 1] = imag[i];\n }\n\n f.transform(out, input);\n\n for (let i = 0; i < n; i++) {\n real[i] = out[i * 2];\n imag[i] = out[i * 2 + 1];\n }\n}\n\n/**\n * Fused FFT → magnitude → decibels for real-valued input.\n * Uses fft.js realTransform (radix-4, ~25% faster for real input).\n * Writes dB values for positive frequencies (n/2 bins) into `out`.\n *\n * @param real - Real input (windowed audio frame, length n)\n * @param out - Output array for dB values (length >= n/2)\n */\nexport function fftMagnitudeDb(real: Float32Array, out: Float32Array): void {\n const n = real.length;\n const f = getFftInstance(n);\n const complexOut = getComplexBuffer(n);\n\n f.realTransform(complexOut, real);\n\n const half = n >> 1;\n for (let i = 0; i < half; i++) {\n const re = complexOut[i * 2];\n const im = complexOut[i * 2 + 1];\n let db = 20 * Math.log10(Math.sqrt(re * re + im * im) + 1e-10);\n if (db < -160) db = -160;\n out[i] = db;\n }\n}\n\n/**\n * Compute magnitude spectrum from FFT output.\n * Returns only the first half (positive frequencies).\n */\nexport function magnitudeSpectrum(real: Float32Array, imag: Float32Array): Float32Array {\n const n = real.length >> 1;\n const magnitudes = new Float32Array(n);\n for (let i = 0; i < n; i++) {\n magnitudes[i] = Math.sqrt(real[i] * real[i] + imag[i] * imag[i]);\n }\n return magnitudes;\n}\n\n/**\n * Convert magnitudes to decibels with a fixed -160 dB floor.\n * Gain is applied at render time, not during FFT.\n */\nexport function toDecibels(magnitudes: Float32Array): Float32Array {\n const result = new Float32Array(magnitudes.length);\n for (let i = 0; i < magnitudes.length; i++) {\n let db = 20 * Math.log10(magnitudes[i] + 1e-10);\n if (db < -160) db = -160;\n result[i] = db;\n }\n return result;\n}\n","/**\n * Window functions for spectral analysis.\n *\n * Uses periodic (DFT-even) windows where the denominator is N, not N-1.\n * Periodic windows tile perfectly over consecutive FFT frames, giving better\n * frequency resolution in STFT/spectrogram computation. This matches the\n * convention used by Audacity, SciPy (sym=False), and MATLAB ('periodic').\n */\n\nexport function getWindowFunction(name: string, size: number, alpha?: number): Float32Array {\n const window = new Float32Array(size);\n const N = size;\n\n switch (name) {\n case 'rectangular':\n for (let i = 0; i < size; i++) window[i] = 1;\n break;\n\n case 'bartlett':\n for (let i = 0; i < size; i++) {\n window[i] = 1 - Math.abs((2 * i - N) / N);\n }\n break;\n\n case 'hann':\n for (let i = 0; i < size; i++) {\n window[i] = 0.5 * (1 - Math.cos((2 * Math.PI * i) / N));\n }\n break;\n\n case 'hamming':\n for (let i = 0; i < size; i++) {\n const a = alpha ?? 0.54;\n window[i] = a - (1 - a) * Math.cos((2 * Math.PI * i) / N);\n }\n break;\n\n case 'blackman': {\n const a0 = 0.42;\n const a1 = 0.5;\n const a2 = 0.08;\n for (let i = 0; i < size; i++) {\n window[i] =\n a0 - a1 * Math.cos((2 * Math.PI * i) / N) + a2 * Math.cos((4 * Math.PI * i) / N);\n }\n break;\n }\n\n case 'blackman-harris': {\n const c0 = 0.35875;\n const c1 = 0.48829;\n const c2 = 0.14128;\n const c3 = 0.01168;\n for (let i = 0; i < size; i++) {\n window[i] =\n c0 -\n c1 * Math.cos((2 * Math.PI * i) / N) +\n c2 * Math.cos((4 * Math.PI * i) / N) -\n c3 * Math.cos((6 * Math.PI * i) / N);\n }\n break;\n }\n\n default:\n console.warn(`[spectrogram] Unknown window function \"${name}\", falling back to hann`);\n for (let i = 0; i < size; i++) {\n window[i] = 0.5 * (1 - Math.cos((2 * Math.PI * i) / N));\n }\n }\n\n // Amplitude normalization: scale so a 0 dB sine produces a 0 dB spectrum peak.\n // Matches Audacity: scale = 2.0 / sum(window)\n let sum = 0;\n for (let i = 0; i < size; i++) sum += window[i];\n if (sum > 0) {\n const scale = 2.0 / sum;\n for (let i = 0; i < size; i++) window[i] *= scale;\n }\n\n return window;\n}\n","/**\n * Frequency scale mapping functions.\n * Each maps a frequency (Hz) to a normalized position [0, 1].\n */\n\nfunction linearScale(f: number, minF: number, maxF: number): number {\n if (maxF === minF) return 0;\n return (f - minF) / (maxF - minF);\n}\n\nfunction logarithmicScale(f: number, minF: number, maxF: number): number {\n const logMin = Math.log2(Math.max(minF, 1));\n const logMax = Math.log2(maxF);\n if (logMax === logMin) return 0;\n return (Math.log2(Math.max(f, 1)) - logMin) / (logMax - logMin);\n}\n\nfunction hzToMel(f: number): number {\n return 2595 * Math.log10(1 + f / 700);\n}\n\nfunction melScale(f: number, minF: number, maxF: number): number {\n const melMin = hzToMel(minF);\n const melMax = hzToMel(maxF);\n if (melMax === melMin) return 0;\n return (hzToMel(f) - melMin) / (melMax - melMin);\n}\n\nfunction hzToBark(f: number): number {\n return 13 * Math.atan(0.00076 * f) + 3.5 * Math.atan((f / 7500) ** 2);\n}\n\nfunction barkScale(f: number, minF: number, maxF: number): number {\n const barkMin = hzToBark(minF);\n const barkMax = hzToBark(maxF);\n if (barkMax === barkMin) return 0;\n return (hzToBark(f) - barkMin) / (barkMax - barkMin);\n}\n\nfunction hzToErb(f: number): number {\n return 21.4 * Math.log10(1 + 0.00437 * f);\n}\n\nfunction erbScale(f: number, minF: number, maxF: number): number {\n const erbMin = hzToErb(minF);\n const erbMax = hzToErb(maxF);\n if (erbMax === erbMin) return 0;\n return (hzToErb(f) - erbMin) / (erbMax - erbMin);\n}\n\nexport type FrequencyScaleName = 'linear' | 'logarithmic' | 'mel' | 'bark' | 'erb';\n\n/**\n * Returns a mapping function: (frequencyHz, minFrequency, maxFrequency) → [0, 1]\n */\nexport function getFrequencyScale(\n name: FrequencyScaleName\n): (f: number, minF: number, maxF: number) => number {\n switch (name) {\n case 'logarithmic':\n return logarithmicScale;\n case 'mel':\n return melScale;\n case 'bark':\n return barkScale;\n case 'erb':\n return erbScale;\n case 'linear':\n return linearScale;\n default:\n console.warn(`[spectrogram] Unknown frequency scale \"${name}\", falling back to linear`);\n return linearScale;\n }\n}\n","/**\n * Web Worker for off-main-thread spectrogram computation and rendering.\n *\n * Supports five modes:\n * 1. `compute` — FFT only, returns SpectrogramData to main thread (backward compat)\n * 2. `register-canvas` / `unregister-canvas` — manage OffscreenCanvas ownership\n * 3. `compute-render` — FFT + direct pixel rendering to registered OffscreenCanvases\n * 4. `compute-fft` — FFT with caching, returns cache key (no rendering)\n * 5. `render-chunks` — render specific chunks from cached FFT data\n */\n\nimport type {\n SpectrogramConfig,\n SpectrogramComputeConfig,\n SpectrogramData,\n} from '@waveform-playlist/core';\nimport { fftMagnitudeDb } from '../computation/fft';\nimport { getWindowFunction } from '../computation/windowFunctions';\nimport { getFrequencyScale, type FrequencyScaleName } from '../computation/frequencyScales';\n\n// --- Canvas registry ---\nconst canvasRegistry = new Map<string, OffscreenCanvas>();\n\n// --- Audio data registry ---\n// Pre-transferred audio data keyed by clipId, avoiding re-transfer on compute-fft.\nconst audioDataRegistry = new Map<\n string,\n { channelDataArrays: Float32Array[]; sampleRate: number }\n>();\n\n// --- FFT cache ---\n// Caches raw dB spectrogram data keyed by FFT computation params.\n// Display-only params (gain, range, colormap) don't affect the cache key.\n// sampleOffset: the sample position where this FFT data starts (for range-limited FFT)\ninterface FFTCacheEntry {\n spectrograms: SpectrogramData[];\n sampleOffset: number;\n}\nconst fftCache = new Map<string, FFTCacheEntry>();\n\nfunction generateCacheKey(params: {\n clipId: string;\n channelIndex: number;\n offsetSamples: number;\n durationSamples: number;\n sampleRate: number;\n compute: SpectrogramComputeConfig;\n mono: boolean;\n}): string {\n const { compute: c } = params;\n return `${params.clipId}:${params.channelIndex}:${params.offsetSamples}:${params.durationSamples}:${params.sampleRate}:${c.fftSize ?? ''}:${c.zeroPaddingFactor ?? ''}:${c.hopSize ?? ''}:${c.windowFunction ?? ''}:${c.alpha ?? ''}:${params.mono ? 1 : 0}`;\n}\n\n// --- Message types ---\n\ninterface ComputeRequest {\n type?: 'compute';\n id: string;\n channelDataArrays: Float32Array[];\n config: SpectrogramConfig;\n sampleRate: number;\n offsetSamples: number;\n durationSamples: number;\n mono: boolean;\n}\n\ninterface RegisterCanvasMessage {\n type: 'register-canvas';\n canvasId: string;\n canvas: OffscreenCanvas;\n}\n\ninterface UnregisterCanvasMessage {\n type: 'unregister-canvas';\n canvasId: string;\n}\n\ninterface ComputeRenderRequest {\n type: 'compute-render';\n id: string;\n channelDataArrays: Float32Array[];\n config: SpectrogramConfig;\n sampleRate: number;\n offsetSamples: number;\n durationSamples: number;\n mono: boolean;\n render: {\n canvasIds: string[][]; // [channel][chunk] → canvasId\n canvasWidths: number[]; // per-chunk CSS widths\n canvasHeight: number;\n devicePixelRatio: number;\n samplesPerPixel: number;\n colorLUT: Uint8Array;\n frequencyScale: string;\n minFrequency: number;\n maxFrequency: number;\n };\n}\n\ninterface RegisterAudioDataMessage {\n type: 'register-audio-data';\n clipId: string;\n channelDataArrays: Float32Array[];\n sampleRate: number;\n}\n\ninterface UnregisterAudioDataMessage {\n type: 'unregister-audio-data';\n clipId: string;\n}\n\ninterface ComputeFFTRequest {\n type: 'compute-fft';\n id: string;\n clipId: string;\n channelDataArrays: Float32Array[];\n config: SpectrogramConfig;\n sampleRate: number;\n offsetSamples: number;\n durationSamples: number;\n mono: boolean;\n sampleRange?: { start: number; end: number };\n}\n\ninterface RenderChunksRequest {\n type: 'render-chunks';\n id: string;\n cacheKey: string;\n canvasIds: string[]; // flat list of canvas IDs to render\n canvasWidths: number[]; // per-chunk CSS widths\n globalPixelOffsets: number[]; // pixel offset for each chunk\n canvasHeight: number;\n devicePixelRatio: number;\n samplesPerPixel: number;\n colorLUT: Uint8Array;\n frequencyScale: string;\n minFrequency: number;\n maxFrequency: number;\n gainDb: number;\n rangeDb: number;\n channelIndex: number;\n}\n\ntype WorkerMessage =\n | ComputeRequest\n | RegisterCanvasMessage\n | UnregisterCanvasMessage\n | ComputeRenderRequest\n | ComputeFFTRequest\n | RenderChunksRequest\n | RegisterAudioDataMessage\n | UnregisterAudioDataMessage;\n\ntype ComputeResponse =\n | { id: string; type: 'spectrograms'; spectrograms: SpectrogramData[] }\n | { id: string; type: 'cache-key'; cacheKey: string }\n | { id: string; type: 'done' }\n | { id: string; type: 'error'; error: string };\n\n// --- FFT computation (unchanged) ---\n\nfunction computeFromChannelData(\n channelData: Float32Array,\n config: SpectrogramConfig,\n sampleRate: number,\n offsetSamples: number,\n durationSamples: number\n): SpectrogramData {\n const windowSize = config.fftSize ?? 2048;\n const zeroPaddingFactor = config.zeroPaddingFactor ?? 2;\n const actualFftSize = windowSize * zeroPaddingFactor;\n const hopSize = config.hopSize ?? Math.floor(windowSize / 4);\n const windowName = config.windowFunction ?? 'hann';\n const gainDb = config.gainDb ?? 20;\n const rangeDb = config.rangeDb ?? 80;\n const alpha = config.alpha;\n\n const frequencyBinCount = actualFftSize >> 1;\n const totalSamples = durationSamples;\n\n const window = getWindowFunction(windowName, windowSize, alpha);\n const frameCount = Math.max(1, Math.floor((totalSamples - windowSize) / hopSize) + 1);\n const data = new Float32Array(frameCount * frequencyBinCount);\n const real = new Float32Array(actualFftSize);\n const dbBuf = new Float32Array(frequencyBinCount);\n\n for (let frame = 0; frame < frameCount; frame++) {\n const start = offsetSamples + frame * hopSize;\n\n for (let i = 0; i < windowSize; i++) {\n const sampleIdx = start + i;\n real[i] = sampleIdx < channelData.length ? channelData[sampleIdx] * window[i] : 0;\n }\n for (let i = windowSize; i < actualFftSize; i++) {\n real[i] = 0;\n }\n\n fftMagnitudeDb(real, dbBuf);\n data.set(dbBuf, frame * frequencyBinCount);\n }\n\n return {\n fftSize: actualFftSize,\n windowSize,\n frequencyBinCount,\n sampleRate,\n hopSize,\n frameCount,\n data,\n gainDb,\n rangeDb,\n };\n}\n\nfunction computeMonoFromChannels(\n channels: Float32Array[],\n config: SpectrogramConfig,\n sampleRate: number,\n offsetSamples: number,\n durationSamples: number\n): SpectrogramData {\n if (channels.length === 1) {\n return computeFromChannelData(channels[0], config, sampleRate, offsetSamples, durationSamples);\n }\n\n const windowSize = config.fftSize ?? 2048;\n const zeroPaddingFactor = config.zeroPaddingFactor ?? 2;\n const actualFftSize = windowSize * zeroPaddingFactor;\n const hopSize = config.hopSize ?? Math.floor(windowSize / 4);\n const windowName = config.windowFunction ?? 'hann';\n const gainDb = config.gainDb ?? 20;\n const rangeDb = config.rangeDb ?? 80;\n const alpha = config.alpha;\n\n const frequencyBinCount = actualFftSize >> 1;\n const numChannels = channels.length;\n\n const window = getWindowFunction(windowName, windowSize, alpha);\n const frameCount = Math.max(1, Math.floor((durationSamples - windowSize) / hopSize) + 1);\n const data = new Float32Array(frameCount * frequencyBinCount);\n const real = new Float32Array(actualFftSize);\n const dbBuf = new Float32Array(frequencyBinCount);\n\n for (let frame = 0; frame < frameCount; frame++) {\n const start = offsetSamples + frame * hopSize;\n\n for (let i = 0; i < windowSize; i++) {\n const sampleIdx = start + i;\n let sum = 0;\n for (let ch = 0; ch < numChannels; ch++) {\n sum += sampleIdx < channels[ch].length ? channels[ch][sampleIdx] : 0;\n }\n real[i] = (sum / numChannels) * window[i];\n }\n for (let i = windowSize; i < actualFftSize; i++) {\n real[i] = 0;\n }\n\n fftMagnitudeDb(real, dbBuf);\n data.set(dbBuf, frame * frequencyBinCount);\n }\n\n return {\n fftSize: actualFftSize,\n windowSize,\n frequencyBinCount,\n sampleRate,\n hopSize,\n frameCount,\n data,\n gainDb,\n rangeDb,\n };\n}\n\n// --- Rendering ---\n\nfunction renderSpectrogramToCanvas(\n specData: SpectrogramData,\n canvasIds: string[],\n canvasWidths: number[],\n canvasHeight: number,\n devicePixelRatio: number,\n samplesPerPixel: number,\n colorLUT: Uint8Array,\n scaleFn: (f: number, minF: number, maxF: number) => number,\n minFrequency: number,\n maxFrequency: number,\n isNonLinear: boolean,\n globalPixelOffsets?: number[],\n gainDbOverride?: number,\n rangeDbOverride?: number,\n sampleOffset = 0\n): void {\n const { frequencyBinCount, frameCount, hopSize, sampleRate } = specData;\n const gainDb = gainDbOverride ?? specData.gainDb;\n const rawRangeDb = rangeDbOverride ?? specData.rangeDb;\n const rangeDb = rawRangeDb === 0 ? 1 : rawRangeDb;\n const maxF = maxFrequency > 0 ? maxFrequency : sampleRate / 2;\n const binToFreq = (bin: number) => (bin / frequencyBinCount) * (sampleRate / 2);\n\n let accumulatedOffset = 0;\n\n for (let chunkIdx = 0; chunkIdx < canvasIds.length; chunkIdx++) {\n const canvasId = canvasIds[chunkIdx];\n const offscreen = canvasRegistry.get(canvasId);\n if (!offscreen) {\n console.warn(`[spectrogram-worker] Canvas \"${canvasId}\" not found in registry`);\n if (!globalPixelOffsets) accumulatedOffset += canvasWidths[chunkIdx];\n continue;\n }\n\n const canvasWidth = canvasWidths[chunkIdx];\n const globalPixelOffset = globalPixelOffsets ? globalPixelOffsets[chunkIdx] : accumulatedOffset;\n\n // Set physical canvas size for DPR\n offscreen.width = canvasWidth * devicePixelRatio;\n offscreen.height = canvasHeight * devicePixelRatio;\n\n const ctx = offscreen.getContext('2d');\n if (!ctx) {\n console.warn(`[spectrogram-worker] getContext('2d') returned null for canvas \"${canvasId}\"`);\n if (!globalPixelOffsets) accumulatedOffset += canvasWidth;\n continue;\n }\n\n ctx.resetTransform();\n ctx.clearRect(0, 0, offscreen.width, offscreen.height);\n ctx.imageSmoothingEnabled = false;\n\n // Create ImageData at CSS pixel size\n const imgData = ctx.createImageData(canvasWidth, canvasHeight);\n const pixels = imgData.data;\n\n for (let x = 0; x < canvasWidth; x++) {\n const globalX = globalPixelOffset + x;\n const samplePos = globalX * samplesPerPixel - sampleOffset;\n const frame = Math.floor(samplePos / hopSize);\n\n if (frame < 0 || frame >= frameCount) continue;\n\n const frameOffset = frame * frequencyBinCount;\n\n for (let y = 0; y < canvasHeight; y++) {\n const normalizedY = 1 - y / canvasHeight;\n\n let bin = Math.floor(normalizedY * frequencyBinCount);\n\n if (isNonLinear) {\n let lo = 0;\n let hi = frequencyBinCount - 1;\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n const freq = binToFreq(mid);\n const scaled = scaleFn(freq, minFrequency, maxF);\n if (scaled < normalizedY) {\n lo = mid + 1;\n } else {\n hi = mid;\n }\n }\n bin = lo;\n }\n\n if (bin < 0 || bin >= frequencyBinCount) continue;\n\n const db = specData.data[frameOffset + bin];\n const normalized = Math.max(0, Math.min(1, (db + rangeDb + gainDb) / rangeDb));\n\n const colorIdx = Math.floor(normalized * 255);\n const pixelIdx = (y * canvasWidth + x) * 4;\n pixels[pixelIdx] = colorLUT[colorIdx * 3];\n pixels[pixelIdx + 1] = colorLUT[colorIdx * 3 + 1];\n pixels[pixelIdx + 2] = colorLUT[colorIdx * 3 + 2];\n pixels[pixelIdx + 3] = 255;\n }\n }\n\n // Put image data and scale up for DPR\n if (devicePixelRatio === 1) {\n ctx.putImageData(imgData, 0, 0);\n } else {\n // Render at CSS size to a temporary OffscreenCanvas, then scale up\n const tmpCanvas = new OffscreenCanvas(canvasWidth, canvasHeight);\n const tmpCtx = tmpCanvas.getContext('2d');\n if (!tmpCtx) continue;\n tmpCtx.putImageData(imgData, 0, 0);\n\n ctx.imageSmoothingEnabled = false;\n ctx.drawImage(tmpCanvas, 0, 0, offscreen.width, offscreen.height);\n }\n\n if (!globalPixelOffsets) accumulatedOffset += canvasWidth;\n }\n}\n\n// --- Message handler ---\n\nself.onmessage = (e: MessageEvent<WorkerMessage>) => {\n const msg = e.data;\n\n // Register canvas\n if (msg.type === 'register-canvas') {\n try {\n canvasRegistry.set(msg.canvasId, msg.canvas);\n } catch (err) {\n console.warn('[spectrogram-worker] register-canvas failed:', err);\n }\n return;\n }\n\n // Unregister canvas\n if (msg.type === 'unregister-canvas') {\n try {\n canvasRegistry.delete(msg.canvasId);\n } catch (err) {\n console.warn('[spectrogram-worker] unregister-canvas failed:', err);\n }\n return;\n }\n\n // Register audio data for a clip (pre-transfer)\n if (msg.type === 'register-audio-data') {\n try {\n audioDataRegistry.set(msg.clipId, {\n channelDataArrays: msg.channelDataArrays,\n sampleRate: msg.sampleRate,\n });\n } catch (err) {\n console.warn('[spectrogram-worker] register-audio-data failed:', err);\n }\n return;\n }\n\n // Unregister audio data for a clip + evict related FFT cache entries\n if (msg.type === 'unregister-audio-data') {\n try {\n audioDataRegistry.delete(msg.clipId);\n const prefix = `${msg.clipId}:`;\n for (const key of fftCache.keys()) {\n if (key.startsWith(prefix)) {\n fftCache.delete(key);\n }\n }\n } catch (err) {\n console.warn('[spectrogram-worker] unregister-audio-data failed:', err);\n }\n return;\n }\n\n // Compute FFT only (with caching), return cache key\n if (msg.type === 'compute-fft') {\n const { id } = msg;\n try {\n const {\n clipId,\n config,\n sampleRate: msgSampleRate,\n offsetSamples,\n durationSamples,\n mono,\n sampleRange,\n } = msg;\n\n // Use pre-registered audio data if available, otherwise use message payload\n const registered = audioDataRegistry.get(clipId);\n const channelDataArrays =\n registered && msg.channelDataArrays.length === 0\n ? registered.channelDataArrays\n : msg.channelDataArrays;\n const sampleRate =\n registered && msg.channelDataArrays.length === 0 ? registered.sampleRate : msgSampleRate;\n\n const fftSize = config.fftSize ?? 2048;\n const zeroPaddingFactor = config.zeroPaddingFactor ?? 2;\n const hopSize = config.hopSize ?? Math.floor(fftSize / 4);\n const windowFunction = config.windowFunction ?? 'hann';\n\n // Use sampleRange if provided (visible-range-first optimization)\n const effectiveOffset = sampleRange ? sampleRange.start : offsetSamples;\n const effectiveDuration = sampleRange ? sampleRange.end - sampleRange.start : durationSamples;\n\n const cacheKey = generateCacheKey({\n clipId,\n channelIndex: 0,\n offsetSamples: effectiveOffset,\n durationSamples: effectiveDuration,\n sampleRate,\n compute: { fftSize, zeroPaddingFactor, hopSize, windowFunction, alpha: config.alpha },\n mono,\n });\n\n if (!fftCache.has(cacheKey)) {\n // Evict stale cache entries for this clip (different FFT params)\n const clipPrefix = `${clipId}:`;\n for (const key of fftCache.keys()) {\n if (key.startsWith(clipPrefix) && key !== cacheKey) {\n fftCache.delete(key);\n }\n }\n\n const spectrograms: SpectrogramData[] = [];\n if (mono || channelDataArrays.length === 1) {\n spectrograms.push(\n computeMonoFromChannels(\n channelDataArrays,\n config,\n sampleRate,\n effectiveOffset,\n effectiveDuration\n )\n );\n } else {\n for (const channelData of channelDataArrays) {\n spectrograms.push(\n computeFromChannelData(\n channelData,\n config,\n sampleRate,\n effectiveOffset,\n effectiveDuration\n )\n );\n }\n }\n fftCache.set(cacheKey, { spectrograms, sampleOffset: effectiveOffset });\n }\n\n const response: ComputeResponse = { id, type: 'cache-key', cacheKey };\n (self as unknown as Worker).postMessage(response);\n } catch (err) {\n const response: ComputeResponse = { id, type: 'error', error: String(err) };\n (self as unknown as Worker).postMessage(response);\n }\n return;\n }\n\n // Render specific chunks from cached FFT data\n if (msg.type === 'render-chunks') {\n const { id } = msg;\n try {\n const {\n cacheKey,\n canvasIds,\n canvasWidths,\n globalPixelOffsets,\n canvasHeight,\n devicePixelRatio,\n samplesPerPixel,\n colorLUT,\n frequencyScale,\n minFrequency,\n maxFrequency,\n gainDb,\n rangeDb,\n channelIndex,\n } = msg;\n\n const cacheEntry = fftCache.get(cacheKey);\n if (!cacheEntry || channelIndex >= cacheEntry.spectrograms.length) {\n const response: ComputeResponse = { id, type: 'error', error: 'cache-miss' };\n (self as unknown as Worker).postMessage(response);\n return;\n }\n\n const scaleFn = getFrequencyScale((frequencyScale ?? 'mel') as FrequencyScaleName);\n const isNonLinear = frequencyScale !== 'linear';\n\n renderSpectrogramToCanvas(\n cacheEntry.spectrograms[channelIndex],\n canvasIds,\n canvasWidths,\n canvasHeight,\n devicePixelRatio,\n samplesPerPixel,\n colorLUT,\n scaleFn,\n minFrequency,\n maxFrequency,\n isNonLinear,\n globalPixelOffsets,\n gainDb,\n rangeDb,\n cacheEntry.sampleOffset\n );\n\n const response: ComputeResponse = { id, type: 'done' };\n (self as unknown as Worker).postMessage(response);\n } catch (err) {\n const response: ComputeResponse = { id, type: 'error', error: String(err) };\n (self as unknown as Worker).postMessage(response);\n }\n return;\n }\n\n // Compute + render to registered canvases (uses cache internally)\n if (msg.type === 'compute-render') {\n const { id } = msg;\n try {\n const {\n channelDataArrays,\n config,\n sampleRate,\n offsetSamples,\n durationSamples,\n mono,\n render,\n } = msg;\n\n // Compute spectrograms\n const spectrograms: SpectrogramData[] = [];\n if (mono || channelDataArrays.length === 1) {\n spectrograms.push(\n computeMonoFromChannels(\n channelDataArrays,\n config,\n sampleRate,\n offsetSamples,\n durationSamples\n )\n );\n } else {\n for (const channelData of channelDataArrays) {\n spectrograms.push(\n computeFromChannelData(channelData, config, sampleRate, offsetSamples, durationSamples)\n );\n }\n }\n\n // Render each channel's spectrogram to its canvas chunks\n const scaleFn = getFrequencyScale((render.frequencyScale ?? 'mel') as FrequencyScaleName);\n const isNonLinear = render.frequencyScale !== 'linear';\n\n for (let ch = 0; ch < spectrograms.length; ch++) {\n const channelCanvasIds = render.canvasIds[ch];\n if (!channelCanvasIds || channelCanvasIds.length === 0) continue;\n\n renderSpectrogramToCanvas(\n spectrograms[ch],\n channelCanvasIds,\n render.canvasWidths,\n render.canvasHeight,\n render.devicePixelRatio,\n render.samplesPerPixel,\n render.colorLUT,\n scaleFn,\n render.minFrequency,\n render.maxFrequency,\n isNonLinear\n );\n }\n\n const response: ComputeResponse = { id, type: 'done' };\n (self as unknown as Worker).postMessage(response);\n } catch (err) {\n const response: ComputeResponse = { id, type: 'error', error: String(err) };\n (self as unknown as Worker).postMessage(response);\n }\n return;\n }\n\n // Legacy compute-only (backward compat — no type field or type === 'compute')\n const { id, channelDataArrays, config, sampleRate, offsetSamples, durationSamples, mono } =\n msg as ComputeRequest;\n try {\n const spectrograms: SpectrogramData[] = [];\n\n if (mono || channelDataArrays.length === 1) {\n spectrograms.push(\n computeMonoFromChannels(\n channelDataArrays,\n config,\n sampleRate,\n offsetSamples,\n durationSamples\n )\n );\n } else {\n for (const channelData of channelDataArrays) {\n spectrograms.push(\n computeFromChannelData(channelData, config, sampleRate, offsetSamples, durationSamples)\n );\n }\n }\n\n // Transfer the data Float32Arrays back (zero-copy)\n const transferables = spectrograms.map((s) => s.data.buffer);\n\n const response: ComputeResponse = { id, type: 'spectrograms', spectrograms };\n (self as unknown as Worker).postMessage(response, transferables);\n } catch (err) {\n const response: ComputeResponse = { id, type: 'error', error: String(err) };\n (self as unknown as Worker).postMessage(response);\n }\n};\n"],"mappings":"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","names":["id","config","offsetSamples","durationSamples","mono","channelDataArrays","sampleRate","response"]}
|
|
1
|
+
{"version":3,"sources":["../../src/computation/fft.ts","../../src/computation/windowFunctions.ts","../../src/computation/frequencyScales.ts","../../src/worker/spectrogram.worker.ts"],"sourcesContent":["import FFT from 'fft.js';\n\n/**\n * Cache fft.js instances per size (pre-computes twiddle factors).\n */\nconst fftInstances = new Map<number, FFT>();\nconst complexBuffers = new Map<number, number[]>();\n\nfunction getFftInstance(size: number): FFT {\n let instance = fftInstances.get(size);\n if (!instance) {\n instance = new FFT(size);\n fftInstances.set(size, instance);\n complexBuffers.set(size, instance.createComplexArray());\n }\n return instance;\n}\n\nfunction getComplexBuffer(size: number): number[] {\n const buffer = complexBuffers.get(size);\n if (!buffer) {\n throw new Error(`No complex buffer for size ${size}. Call getFftInstance first.`);\n }\n return buffer;\n}\n\n/**\n * In-place FFT using fft.js (radix-4).\n * @param real - Real part (modified in place)\n * @param imag - Imaginary part (modified in place)\n */\nexport function fft(real: Float32Array, imag: Float32Array): void {\n const n = real.length;\n const f = getFftInstance(n);\n const input = f.createComplexArray();\n const out = getComplexBuffer(n);\n\n for (let i = 0; i < n; i++) {\n input[i * 2] = real[i];\n input[i * 2 + 1] = imag[i];\n }\n\n f.transform(out, input);\n\n for (let i = 0; i < n; i++) {\n real[i] = out[i * 2];\n imag[i] = out[i * 2 + 1];\n }\n}\n\n/**\n * Fused FFT → magnitude → decibels for real-valued input.\n * Uses fft.js realTransform (radix-4, ~25% faster for real input).\n * Writes dB values for positive frequencies (n/2 bins) into `out`.\n *\n * @param real - Real input (windowed audio frame, length n)\n * @param out - Output array for dB values (length >= n/2)\n */\nexport function fftMagnitudeDb(real: Float32Array, out: Float32Array): void {\n const n = real.length;\n const f = getFftInstance(n);\n const complexOut = getComplexBuffer(n);\n\n f.realTransform(complexOut, real);\n\n const half = n >> 1;\n for (let i = 0; i < half; i++) {\n const re = complexOut[i * 2];\n const im = complexOut[i * 2 + 1];\n let db = 20 * Math.log10(Math.sqrt(re * re + im * im) + 1e-10);\n if (db < -160) db = -160;\n out[i] = db;\n }\n}\n\n/**\n * Compute magnitude spectrum from FFT output.\n * Returns only the first half (positive frequencies).\n */\nexport function magnitudeSpectrum(real: Float32Array, imag: Float32Array): Float32Array {\n const n = real.length >> 1;\n const magnitudes = new Float32Array(n);\n for (let i = 0; i < n; i++) {\n magnitudes[i] = Math.sqrt(real[i] * real[i] + imag[i] * imag[i]);\n }\n return magnitudes;\n}\n\n/**\n * Convert magnitudes to decibels with a fixed -160 dB floor.\n * Gain is applied at render time, not during FFT.\n */\nexport function toDecibels(magnitudes: Float32Array): Float32Array {\n const result = new Float32Array(magnitudes.length);\n for (let i = 0; i < magnitudes.length; i++) {\n let db = 20 * Math.log10(magnitudes[i] + 1e-10);\n if (db < -160) db = -160;\n result[i] = db;\n }\n return result;\n}\n","/**\n * Window functions for spectral analysis.\n *\n * Uses periodic (DFT-even) windows where the denominator is N, not N-1.\n * Periodic windows tile perfectly over consecutive FFT frames, giving better\n * frequency resolution in STFT/spectrogram computation. This matches the\n * convention used by Audacity, SciPy (sym=False), and MATLAB ('periodic').\n */\n\nexport function getWindowFunction(name: string, size: number, alpha?: number): Float32Array {\n const window = new Float32Array(size);\n const N = size;\n\n switch (name) {\n case 'rectangular':\n for (let i = 0; i < size; i++) window[i] = 1;\n break;\n\n case 'bartlett':\n for (let i = 0; i < size; i++) {\n window[i] = 1 - Math.abs((2 * i - N) / N);\n }\n break;\n\n case 'hann':\n for (let i = 0; i < size; i++) {\n window[i] = 0.5 * (1 - Math.cos((2 * Math.PI * i) / N));\n }\n break;\n\n case 'hamming':\n for (let i = 0; i < size; i++) {\n const a = alpha ?? 0.54;\n window[i] = a - (1 - a) * Math.cos((2 * Math.PI * i) / N);\n }\n break;\n\n case 'blackman': {\n const a0 = 0.42;\n const a1 = 0.5;\n const a2 = 0.08;\n for (let i = 0; i < size; i++) {\n window[i] =\n a0 - a1 * Math.cos((2 * Math.PI * i) / N) + a2 * Math.cos((4 * Math.PI * i) / N);\n }\n break;\n }\n\n case 'blackman-harris': {\n const c0 = 0.35875;\n const c1 = 0.48829;\n const c2 = 0.14128;\n const c3 = 0.01168;\n for (let i = 0; i < size; i++) {\n window[i] =\n c0 -\n c1 * Math.cos((2 * Math.PI * i) / N) +\n c2 * Math.cos((4 * Math.PI * i) / N) -\n c3 * Math.cos((6 * Math.PI * i) / N);\n }\n break;\n }\n\n default:\n console.warn(`[spectrogram] Unknown window function \"${name}\", falling back to hann`);\n for (let i = 0; i < size; i++) {\n window[i] = 0.5 * (1 - Math.cos((2 * Math.PI * i) / N));\n }\n }\n\n // Amplitude normalization: scale so a 0 dB sine produces a 0 dB spectrum peak.\n // Matches Audacity: scale = 2.0 / sum(window)\n let sum = 0;\n for (let i = 0; i < size; i++) sum += window[i];\n if (sum > 0) {\n const scale = 2.0 / sum;\n for (let i = 0; i < size; i++) window[i] *= scale;\n }\n\n return window;\n}\n","/**\n * Frequency scale mapping functions.\n * Each maps a frequency (Hz) to a normalized position [0, 1].\n */\n\nfunction linearScale(f: number, minF: number, maxF: number): number {\n if (maxF === minF) return 0;\n return (f - minF) / (maxF - minF);\n}\n\nfunction logarithmicScale(f: number, minF: number, maxF: number): number {\n const logMin = Math.log2(Math.max(minF, 1));\n const logMax = Math.log2(maxF);\n if (logMax === logMin) return 0;\n return (Math.log2(Math.max(f, 1)) - logMin) / (logMax - logMin);\n}\n\nfunction hzToMel(f: number): number {\n return 2595 * Math.log10(1 + f / 700);\n}\n\nfunction melScale(f: number, minF: number, maxF: number): number {\n const melMin = hzToMel(minF);\n const melMax = hzToMel(maxF);\n if (melMax === melMin) return 0;\n return (hzToMel(f) - melMin) / (melMax - melMin);\n}\n\nfunction hzToBark(f: number): number {\n return 13 * Math.atan(0.00076 * f) + 3.5 * Math.atan((f / 7500) ** 2);\n}\n\nfunction barkScale(f: number, minF: number, maxF: number): number {\n const barkMin = hzToBark(minF);\n const barkMax = hzToBark(maxF);\n if (barkMax === barkMin) return 0;\n return (hzToBark(f) - barkMin) / (barkMax - barkMin);\n}\n\nfunction hzToErb(f: number): number {\n return 21.4 * Math.log10(1 + 0.00437 * f);\n}\n\nfunction erbScale(f: number, minF: number, maxF: number): number {\n const erbMin = hzToErb(minF);\n const erbMax = hzToErb(maxF);\n if (erbMax === erbMin) return 0;\n return (hzToErb(f) - erbMin) / (erbMax - erbMin);\n}\n\nexport type FrequencyScaleName = 'linear' | 'logarithmic' | 'mel' | 'bark' | 'erb';\n\n/**\n * Returns a mapping function: (frequencyHz, minFrequency, maxFrequency) → [0, 1]\n */\nexport function getFrequencyScale(\n name: FrequencyScaleName\n): (f: number, minF: number, maxF: number) => number {\n switch (name) {\n case 'logarithmic':\n return logarithmicScale;\n case 'mel':\n return melScale;\n case 'bark':\n return barkScale;\n case 'erb':\n return erbScale;\n case 'linear':\n return linearScale;\n default:\n console.warn(`[spectrogram] Unknown frequency scale \"${name}\", falling back to linear`);\n return linearScale;\n }\n}\n","/**\n * Web Worker for off-main-thread spectrogram computation and rendering.\n *\n * Supports:\n * 1. `register-canvas` / `unregister-canvas` — manage OffscreenCanvas ownership\n * 2. `register-audio-data` / `unregister-audio-data` — pre-transfer clip audio data to avoid re-transfer on each FFT request\n * 3. `compute-fft` — FFT with LRU caching, returns cache key (no rendering)\n * 4. `render-chunks` — render specific chunks from cached FFT data\n * 5. `abort-generation` — cancel stale FFT computations cooperatively\n */\n\nimport type {\n SpectrogramConfig,\n SpectrogramComputeConfig,\n SpectrogramData,\n} from '@waveform-playlist/core';\nimport { fftMagnitudeDb } from '../computation/fft';\nimport { getWindowFunction } from '../computation/windowFunctions';\nimport { getFrequencyScale, type FrequencyScaleName } from '../computation/frequencyScales';\n\n// --- Canvas registry ---\nconst canvasRegistry = new Map<string, OffscreenCanvas>();\n\n// --- Audio data registry ---\n// Pre-transferred audio data keyed by clipId, avoiding re-transfer on compute-fft.\nconst audioDataRegistry = new Map<\n string,\n { channelDataArrays: Float32Array[]; sampleRate: number }\n>();\n\n// --- FFT cache ---\n// Caches raw dB spectrogram data keyed by FFT computation params.\n// Display-only params (gain, range, colormap) don't affect the cache key.\n// sampleOffset: the sample position where this FFT data starts (for range-limited FFT)\n// Bounded to MAX_CACHE_ENTRIES to prevent OOM on long files with many ranges.\ninterface FFTCacheEntry {\n spectrograms: SpectrogramData[];\n sampleOffset: number;\n}\nconst MAX_CACHE_ENTRIES = 16;\nconst fftCache = new Map<string, FFTCacheEntry>();\n\nfunction evictLRUCacheEntries(keepKey?: string) {\n while (fftCache.size >= MAX_CACHE_ENTRIES) {\n let deleted = false;\n for (const key of fftCache.keys()) {\n if (key !== keepKey) {\n fftCache.delete(key);\n deleted = true;\n break;\n }\n }\n if (!deleted) break; // Safety: prevent infinite loop if keepKey is the only entry\n }\n}\n\n/** Bump a cache entry to most-recently-used by re-inserting it. */\nfunction touchCacheEntry(key: string) {\n const entry = fftCache.get(key);\n if (entry) {\n fftCache.delete(key);\n fftCache.set(key, entry);\n }\n}\n\nfunction generateCacheKey(params: {\n clipId: string;\n channelIndex: number;\n offsetSamples: number;\n durationSamples: number;\n sampleRate: number;\n compute: SpectrogramComputeConfig;\n mono: boolean;\n}): string {\n const { compute: c } = params;\n return `${params.clipId}:${params.channelIndex}:${params.offsetSamples}:${params.durationSamples}:${params.sampleRate}:${c.fftSize ?? ''}:${c.zeroPaddingFactor ?? ''}:${c.hopSize ?? ''}:${c.windowFunction ?? ''}:${c.alpha ?? ''}:${params.mono ? 1 : 0}`;\n}\n\n// --- Message types ---\n\ninterface RegisterCanvasMessage {\n type: 'register-canvas';\n canvasId: string;\n canvas: OffscreenCanvas;\n}\n\ninterface UnregisterCanvasMessage {\n type: 'unregister-canvas';\n canvasId: string;\n}\n\ninterface RegisterAudioDataMessage {\n type: 'register-audio-data';\n clipId: string;\n channelDataArrays: Float32Array[];\n sampleRate: number;\n}\n\ninterface UnregisterAudioDataMessage {\n type: 'unregister-audio-data';\n clipId: string;\n}\n\ninterface AbortGenerationMessage {\n type: 'abort-generation';\n generation: number;\n}\n\ninterface ComputeFFTRequest {\n type: 'compute-fft';\n id: string;\n generation: number;\n clipId: string;\n channelDataArrays: Float32Array[];\n config: SpectrogramConfig;\n sampleRate: number;\n offsetSamples: number;\n durationSamples: number;\n mono: boolean;\n sampleRange?: { start: number; end: number };\n /** If set, compute only this channel index (not all channels). */\n channelFilter?: number;\n}\n\ninterface RenderChunksRequest {\n type: 'render-chunks';\n id: string;\n generation: number;\n cacheKey: string;\n canvasIds: string[]; // flat list of canvas IDs to render\n canvasWidths: number[]; // per-chunk CSS widths\n globalPixelOffsets: number[]; // pixel offset for each chunk\n canvasHeight: number;\n devicePixelRatio: number;\n samplesPerPixel: number;\n colorLUT: Uint8Array;\n frequencyScale: string;\n minFrequency: number;\n maxFrequency: number;\n gainDb: number;\n rangeDb: number;\n channelIndex: number;\n}\n\ntype WorkerMessage =\n | RegisterCanvasMessage\n | UnregisterCanvasMessage\n | ComputeFFTRequest\n | RenderChunksRequest\n | RegisterAudioDataMessage\n | UnregisterAudioDataMessage\n | AbortGenerationMessage;\n\ntype ComputeResponse =\n | { id: string; type: 'cache-key'; cacheKey: string }\n | { id: string; type: 'done' }\n | { id: string; type: 'aborted' }\n | { id: string; type: 'error'; error: string };\n\n// --- Generation tracking ---\n// The main thread sends abort-generation messages when a new computation\n// generation starts. The worker tracks the latest generation and aborts\n// stale FFT computations by yielding periodically to process messages.\nlet latestGeneration = 0;\nconst FRAMES_PER_YIELD = 2000;\n\nfunction isGenerationStale(generation: number): boolean {\n return generation < latestGeneration;\n}\n\nfunction yieldToMessageQueue(): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, 0));\n}\n\n// --- FFT computation (async with abort support) ---\n\nasync function computeFromChannelData(\n channelData: Float32Array,\n config: SpectrogramConfig,\n sampleRate: number,\n offsetSamples: number,\n durationSamples: number,\n generation: number\n): Promise<SpectrogramData | null> {\n const windowSize = config.fftSize ?? 2048;\n const zeroPaddingFactor = config.zeroPaddingFactor ?? 2;\n const actualFftSize = windowSize * zeroPaddingFactor;\n const hopSize = config.hopSize ?? Math.floor(windowSize / 4);\n const windowName = config.windowFunction ?? 'hann';\n const gainDb = config.gainDb ?? 20;\n const rangeDb = config.rangeDb ?? 80;\n const alpha = config.alpha;\n\n const frequencyBinCount = actualFftSize >> 1;\n const totalSamples = durationSamples;\n\n const window = getWindowFunction(windowName, windowSize, alpha);\n const frameCount = Math.max(1, Math.floor((totalSamples - windowSize) / hopSize) + 1);\n const data = new Float32Array(frameCount * frequencyBinCount);\n const real = new Float32Array(actualFftSize);\n const dbBuf = new Float32Array(frequencyBinCount);\n\n for (let frame = 0; frame < frameCount; frame++) {\n // Yield periodically to process abort messages\n if (frame > 0 && frame % FRAMES_PER_YIELD === 0) {\n await yieldToMessageQueue();\n if (isGenerationStale(generation)) return null;\n }\n\n const start = offsetSamples + frame * hopSize;\n\n for (let i = 0; i < windowSize; i++) {\n const sampleIdx = start + i;\n real[i] = sampleIdx < channelData.length ? channelData[sampleIdx] * window[i] : 0;\n }\n for (let i = windowSize; i < actualFftSize; i++) {\n real[i] = 0;\n }\n\n fftMagnitudeDb(real, dbBuf);\n data.set(dbBuf, frame * frequencyBinCount);\n }\n\n return {\n fftSize: actualFftSize,\n windowSize,\n frequencyBinCount,\n sampleRate,\n hopSize,\n frameCount,\n data,\n gainDb,\n rangeDb,\n };\n}\n\nasync function computeMonoFromChannels(\n channels: Float32Array[],\n config: SpectrogramConfig,\n sampleRate: number,\n offsetSamples: number,\n durationSamples: number,\n generation: number\n): Promise<SpectrogramData | null> {\n if (channels.length === 1) {\n return computeFromChannelData(\n channels[0],\n config,\n sampleRate,\n offsetSamples,\n durationSamples,\n generation\n );\n }\n\n const windowSize = config.fftSize ?? 2048;\n const zeroPaddingFactor = config.zeroPaddingFactor ?? 2;\n const actualFftSize = windowSize * zeroPaddingFactor;\n const hopSize = config.hopSize ?? Math.floor(windowSize / 4);\n const windowName = config.windowFunction ?? 'hann';\n const gainDb = config.gainDb ?? 20;\n const rangeDb = config.rangeDb ?? 80;\n const alpha = config.alpha;\n\n const frequencyBinCount = actualFftSize >> 1;\n const numChannels = channels.length;\n\n const window = getWindowFunction(windowName, windowSize, alpha);\n const frameCount = Math.max(1, Math.floor((durationSamples - windowSize) / hopSize) + 1);\n const data = new Float32Array(frameCount * frequencyBinCount);\n const real = new Float32Array(actualFftSize);\n const dbBuf = new Float32Array(frequencyBinCount);\n\n for (let frame = 0; frame < frameCount; frame++) {\n // Yield periodically to process abort messages\n if (frame > 0 && frame % FRAMES_PER_YIELD === 0) {\n await yieldToMessageQueue();\n if (isGenerationStale(generation)) return null;\n }\n\n const start = offsetSamples + frame * hopSize;\n\n for (let i = 0; i < windowSize; i++) {\n const sampleIdx = start + i;\n let sum = 0;\n for (let ch = 0; ch < numChannels; ch++) {\n sum += sampleIdx < channels[ch].length ? channels[ch][sampleIdx] : 0;\n }\n real[i] = (sum / numChannels) * window[i];\n }\n for (let i = windowSize; i < actualFftSize; i++) {\n real[i] = 0;\n }\n\n fftMagnitudeDb(real, dbBuf);\n data.set(dbBuf, frame * frequencyBinCount);\n }\n\n return {\n fftSize: actualFftSize,\n windowSize,\n frequencyBinCount,\n sampleRate,\n hopSize,\n frameCount,\n data,\n gainDb,\n rangeDb,\n };\n}\n\n// --- Rendering ---\n\nfunction renderSpectrogramToCanvas(\n specData: SpectrogramData,\n canvasIds: string[],\n canvasWidths: number[],\n canvasHeight: number,\n devicePixelRatio: number,\n samplesPerPixel: number,\n colorLUT: Uint8Array,\n scaleFn: (f: number, minF: number, maxF: number) => number,\n minFrequency: number,\n maxFrequency: number,\n isNonLinear: boolean,\n globalPixelOffsets?: number[],\n gainDbOverride?: number,\n rangeDbOverride?: number,\n sampleOffset = 0\n): void {\n const { frequencyBinCount, frameCount, hopSize, sampleRate } = specData;\n const gainDb = gainDbOverride ?? specData.gainDb;\n const rawRangeDb = rangeDbOverride ?? specData.rangeDb;\n const rangeDb = rawRangeDb === 0 ? 1 : rawRangeDb;\n const maxF = maxFrequency > 0 ? maxFrequency : sampleRate / 2;\n const binToFreq = (bin: number) => (bin / frequencyBinCount) * (sampleRate / 2);\n\n let accumulatedOffset = 0;\n\n for (let chunkIdx = 0; chunkIdx < canvasIds.length; chunkIdx++) {\n const canvasId = canvasIds[chunkIdx];\n const offscreen = canvasRegistry.get(canvasId);\n if (!offscreen) {\n console.warn(`[spectrogram-worker] Canvas \"${canvasId}\" not found in registry`);\n if (!globalPixelOffsets) accumulatedOffset += canvasWidths[chunkIdx];\n continue;\n }\n\n const canvasWidth = canvasWidths[chunkIdx];\n const globalPixelOffset = globalPixelOffsets ? globalPixelOffsets[chunkIdx] : accumulatedOffset;\n\n // Set physical canvas size for DPR\n offscreen.width = canvasWidth * devicePixelRatio;\n offscreen.height = canvasHeight * devicePixelRatio;\n\n const ctx = offscreen.getContext('2d');\n if (!ctx) {\n console.warn(`[spectrogram-worker] getContext('2d') returned null for canvas \"${canvasId}\"`);\n if (!globalPixelOffsets) accumulatedOffset += canvasWidth;\n continue;\n }\n\n ctx.resetTransform();\n ctx.clearRect(0, 0, offscreen.width, offscreen.height);\n ctx.imageSmoothingEnabled = false;\n\n // Create ImageData at CSS pixel size\n const imgData = ctx.createImageData(canvasWidth, canvasHeight);\n const pixels = imgData.data;\n\n for (let x = 0; x < canvasWidth; x++) {\n const globalX = globalPixelOffset + x;\n const samplePos = globalX * samplesPerPixel - sampleOffset;\n const frame = Math.floor(samplePos / hopSize);\n\n if (frame < 0 || frame >= frameCount) continue;\n\n const frameOffset = frame * frequencyBinCount;\n\n for (let y = 0; y < canvasHeight; y++) {\n const normalizedY = 1 - y / canvasHeight;\n\n let bin = Math.floor(normalizedY * frequencyBinCount);\n\n if (isNonLinear) {\n let lo = 0;\n let hi = frequencyBinCount - 1;\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n const freq = binToFreq(mid);\n const scaled = scaleFn(freq, minFrequency, maxF);\n if (scaled < normalizedY) {\n lo = mid + 1;\n } else {\n hi = mid;\n }\n }\n bin = lo;\n }\n\n if (bin < 0 || bin >= frequencyBinCount) continue;\n\n const db = specData.data[frameOffset + bin];\n const normalized = Math.max(0, Math.min(1, (db + rangeDb + gainDb) / rangeDb));\n\n const colorIdx = Math.floor(normalized * 255);\n const pixelIdx = (y * canvasWidth + x) * 4;\n pixels[pixelIdx] = colorLUT[colorIdx * 3];\n pixels[pixelIdx + 1] = colorLUT[colorIdx * 3 + 1];\n pixels[pixelIdx + 2] = colorLUT[colorIdx * 3 + 2];\n pixels[pixelIdx + 3] = 255;\n }\n }\n\n // Put image data and scale up for DPR\n if (devicePixelRatio === 1) {\n ctx.putImageData(imgData, 0, 0);\n } else {\n // Render at CSS size to a temporary OffscreenCanvas, then scale up\n const tmpCanvas = new OffscreenCanvas(canvasWidth, canvasHeight);\n const tmpCtx = tmpCanvas.getContext('2d');\n if (!tmpCtx) {\n console.warn(\n `[spectrogram-worker] getContext('2d') failed for DPR scaling of \"${canvasIds[chunkIdx]}\"`\n );\n continue;\n }\n tmpCtx.putImageData(imgData, 0, 0);\n\n ctx.imageSmoothingEnabled = false;\n ctx.drawImage(tmpCanvas, 0, 0, offscreen.width, offscreen.height);\n }\n\n if (!globalPixelOffsets) accumulatedOffset += canvasWidth;\n }\n}\n\n// --- Async compute-fft handler ---\n\nasync function handleComputeFFT(msg: ComputeFFTRequest): Promise<void> {\n const {\n id,\n generation,\n clipId,\n config,\n sampleRate: msgSampleRate,\n offsetSamples,\n durationSamples,\n mono,\n sampleRange,\n channelFilter,\n } = msg;\n\n // Use pre-registered audio data if available, otherwise use message payload\n const registered = audioDataRegistry.get(clipId);\n const channelDataArrays =\n registered && msg.channelDataArrays.length === 0\n ? registered.channelDataArrays\n : msg.channelDataArrays;\n const sampleRate =\n registered && msg.channelDataArrays.length === 0 ? registered.sampleRate : msgSampleRate;\n\n const fftSize = config.fftSize ?? 2048;\n const zeroPaddingFactor = config.zeroPaddingFactor ?? 2;\n const hopSize = config.hopSize ?? Math.floor(fftSize / 4);\n const windowFunction = config.windowFunction ?? 'hann';\n\n // Use sampleRange if provided (visible-range-first optimization)\n const effectiveOffset = sampleRange ? sampleRange.start : offsetSamples;\n const effectiveDuration = sampleRange ? sampleRange.end - sampleRange.start : durationSamples;\n\n const cacheKey = generateCacheKey({\n clipId,\n channelIndex: 0,\n offsetSamples: effectiveOffset,\n durationSamples: effectiveDuration,\n sampleRate,\n compute: { fftSize, zeroPaddingFactor, hopSize, windowFunction, alpha: config.alpha },\n mono,\n });\n\n if (!fftCache.has(cacheKey)) {\n // Evict oldest cache entries if at capacity\n evictLRUCacheEntries(cacheKey);\n\n const spectrograms: SpectrogramData[] = [];\n if (mono || channelDataArrays.length === 1) {\n const result = await computeMonoFromChannels(\n channelDataArrays,\n config,\n sampleRate,\n effectiveOffset,\n effectiveDuration,\n generation\n );\n if (result === null) {\n const response: ComputeResponse = { id, type: 'aborted' };\n (self as unknown as Worker).postMessage(response);\n return;\n }\n spectrograms.push(result);\n } else if (channelFilter !== undefined) {\n // Pool mode: compute only the requested channel\n const channelData = channelDataArrays[channelFilter];\n if (!channelData) {\n const response: ComputeResponse = {\n id,\n type: 'error',\n error: `channelFilter ${channelFilter} out of range (${channelDataArrays.length} channels)`,\n };\n (self as unknown as Worker).postMessage(response);\n return;\n }\n const result = await computeFromChannelData(\n channelData,\n config,\n sampleRate,\n effectiveOffset,\n effectiveDuration,\n generation\n );\n if (result === null) {\n const response: ComputeResponse = { id, type: 'aborted' };\n (self as unknown as Worker).postMessage(response);\n return;\n }\n spectrograms.push(result);\n } else {\n for (const channelData of channelDataArrays) {\n const result = await computeFromChannelData(\n channelData,\n config,\n sampleRate,\n effectiveOffset,\n effectiveDuration,\n generation\n );\n if (result === null) {\n const response: ComputeResponse = { id, type: 'aborted' };\n (self as unknown as Worker).postMessage(response);\n return;\n }\n spectrograms.push(result);\n }\n }\n fftCache.set(cacheKey, { spectrograms, sampleOffset: effectiveOffset });\n } else {\n // Bump to most-recently-used so scroll-back patterns keep hot entries alive\n touchCacheEntry(cacheKey);\n }\n\n const response: ComputeResponse = { id, type: 'cache-key', cacheKey };\n (self as unknown as Worker).postMessage(response);\n}\n\n// --- Message handler ---\n\nself.onmessage = (e: MessageEvent<WorkerMessage>) => {\n const msg = e.data;\n\n // Register canvas\n if (msg.type === 'register-canvas') {\n try {\n canvasRegistry.set(msg.canvasId, msg.canvas);\n } catch (err) {\n console.warn('[spectrogram-worker] register-canvas failed:', err);\n }\n return;\n }\n\n // Unregister canvas\n if (msg.type === 'unregister-canvas') {\n try {\n canvasRegistry.delete(msg.canvasId);\n } catch (err) {\n console.warn('[spectrogram-worker] unregister-canvas failed:', err);\n }\n return;\n }\n\n // Register audio data for a clip (pre-transfer)\n if (msg.type === 'register-audio-data') {\n try {\n audioDataRegistry.set(msg.clipId, {\n channelDataArrays: msg.channelDataArrays,\n sampleRate: msg.sampleRate,\n });\n } catch (err) {\n console.warn('[spectrogram-worker] register-audio-data failed:', err);\n }\n return;\n }\n\n // Unregister audio data for a clip + evict related FFT cache entries\n if (msg.type === 'unregister-audio-data') {\n try {\n audioDataRegistry.delete(msg.clipId);\n const prefix = `${msg.clipId}:`;\n for (const key of fftCache.keys()) {\n if (key.startsWith(prefix)) {\n fftCache.delete(key);\n }\n }\n } catch (err) {\n console.warn('[spectrogram-worker] unregister-audio-data failed:', err);\n }\n return;\n }\n\n // Abort stale generation — updates latestGeneration so in-flight async\n // FFT computations will detect staleness and bail out at their next yield point.\n if (msg.type === 'abort-generation') {\n latestGeneration = Math.max(latestGeneration, msg.generation);\n return;\n }\n\n // Compute FFT only (with caching), return cache key\n if (msg.type === 'compute-fft') {\n const { id, generation } = msg;\n\n // Check staleness immediately before starting any work\n if (isGenerationStale(generation)) {\n const response: ComputeResponse = { id, type: 'aborted' };\n (self as unknown as Worker).postMessage(response);\n return;\n }\n\n handleComputeFFT(msg).catch((err) => {\n const errorMsg = err instanceof Error ? `${err.message}\\n${err.stack}` : String(err);\n const response: ComputeResponse = { id, type: 'error', error: errorMsg };\n (self as unknown as Worker).postMessage(response);\n });\n return;\n }\n\n // Render specific chunks from cached FFT data\n if (msg.type === 'render-chunks') {\n const { id, generation } = msg;\n\n // Skip rendering if this request belongs to a stale generation\n if (isGenerationStale(generation)) {\n const response: ComputeResponse = { id, type: 'aborted' };\n (self as unknown as Worker).postMessage(response);\n return;\n }\n\n try {\n const {\n cacheKey,\n canvasIds,\n canvasWidths,\n globalPixelOffsets,\n canvasHeight,\n devicePixelRatio,\n samplesPerPixel,\n colorLUT,\n frequencyScale,\n minFrequency,\n maxFrequency,\n gainDb,\n rangeDb,\n channelIndex,\n } = msg;\n\n const cacheEntry = fftCache.get(cacheKey);\n if (!cacheEntry) {\n const response: ComputeResponse = {\n id,\n type: 'error',\n error: `cache-miss: key \"${cacheKey}\" not found (cache has ${fftCache.size} entries)`,\n };\n (self as unknown as Worker).postMessage(response);\n return;\n }\n if (channelIndex >= cacheEntry.spectrograms.length) {\n const response: ComputeResponse = {\n id,\n type: 'error',\n error: `cache-miss: channelIndex ${channelIndex} out of range (${cacheEntry.spectrograms.length} channels cached)`,\n };\n (self as unknown as Worker).postMessage(response);\n return;\n }\n\n const scaleFn = getFrequencyScale((frequencyScale ?? 'mel') as FrequencyScaleName);\n const isNonLinear = frequencyScale !== 'linear';\n\n renderSpectrogramToCanvas(\n cacheEntry.spectrograms[channelIndex],\n canvasIds,\n canvasWidths,\n canvasHeight,\n devicePixelRatio,\n samplesPerPixel,\n colorLUT,\n scaleFn,\n minFrequency,\n maxFrequency,\n isNonLinear,\n globalPixelOffsets,\n gainDb,\n rangeDb,\n cacheEntry.sampleOffset\n );\n\n const response: ComputeResponse = { id, type: 'done' };\n (self as unknown as Worker).postMessage(response);\n } catch (err) {\n const response: ComputeResponse = { id, type: 'error', error: String(err) };\n (self as unknown as Worker).postMessage(response);\n }\n return;\n }\n\n // Unknown message type\n console.warn(`[spectrogram-worker] Unknown message type: ${(msg as { type?: string }).type}`);\n};\n"],"mappings":"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","names":["response"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@waveform-playlist/spectrogram",
|
|
3
|
-
"version": "9.5.
|
|
3
|
+
"version": "9.5.1",
|
|
4
4
|
"description": "Spectrogram computation and UI for waveform-playlist",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -44,16 +44,16 @@
|
|
|
44
44
|
"tsup": "^8.0.1",
|
|
45
45
|
"vitest": "^3.0.0",
|
|
46
46
|
"typescript": "^5.3.3",
|
|
47
|
-
"@waveform-playlist/browser": "9.5.
|
|
47
|
+
"@waveform-playlist/browser": "9.5.1"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"fft.js": "^4.0.4",
|
|
51
|
-
"@waveform-playlist/core": "9.5.
|
|
51
|
+
"@waveform-playlist/core": "9.5.1"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"react": "^18.0.0",
|
|
55
55
|
"styled-components": "^6.0.0",
|
|
56
|
-
"@waveform-playlist/browser": "9.5.
|
|
56
|
+
"@waveform-playlist/browser": "9.5.1"
|
|
57
57
|
},
|
|
58
58
|
"scripts": {
|
|
59
59
|
"build": "tsup",
|