@waveform-playlist/spectrogram 9.4.1 → 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.
@@ -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
- function computeFromChannelData(channelData, config, sampleRate, offsetSamples, durationSamples) {
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(channels[0], config, sampleRate, offsetSamples, durationSamples);
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) continue;
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: id2 } = msg;
369
- try {
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: id2 } = msg;
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 || channelIndex >= cacheEntry.spectrograms.length) {
459
- const response2 = { id: id2, type: "error", error: "cache-miss" };
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: id2, type: "done" };
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: id2, type: "error", error: String(err) };
599
+ const response = { id, type: "error", error: String(err) };
543
600
  self.postMessage(response);
544
601
  }
545
602
  return;
546
603
  }
547
- const { id, channelDataArrays, config, sampleRate, offsetSamples, durationSamples, mono } = msg;
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.4.1",
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.4.1"
47
+ "@waveform-playlist/browser": "9.5.1"
48
48
  },
49
49
  "dependencies": {
50
50
  "fft.js": "^4.0.4",
51
- "@waveform-playlist/core": "9.4.1"
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.4.1"
56
+ "@waveform-playlist/browser": "9.5.1"
57
57
  },
58
58
  "scripts": {
59
59
  "build": "tsup",