@waveform-playlist/spectrogram 6.0.1 → 7.0.0
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.
|
@@ -12,7 +12,11 @@ function getFftInstance(size) {
|
|
|
12
12
|
return instance;
|
|
13
13
|
}
|
|
14
14
|
function getComplexBuffer(size) {
|
|
15
|
-
|
|
15
|
+
const buffer = complexBuffers.get(size);
|
|
16
|
+
if (!buffer) {
|
|
17
|
+
throw new Error(`No complex buffer for size ${size}. Call getFftInstance first.`);
|
|
18
|
+
}
|
|
19
|
+
return buffer;
|
|
16
20
|
}
|
|
17
21
|
function fftMagnitudeDb(real, out) {
|
|
18
22
|
const n = real.length;
|
|
@@ -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, any>();\nconst complexBuffers = new Map<number, any>();\n\nfunction getFftInstance(size: number): any {\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): any {\n return complexBuffers.get(size);\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\nexport function getWindowFunction(\n name: string,\n size: number,\n alpha?: number\n): 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 -\n a1 * Math.cos((2 * Math.PI * i) / N) +\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': return logarithmicScale;\n case 'mel': return melScale;\n case 'bark': return barkScale;\n case 'erb': 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 { SpectrogramConfig, SpectrogramComputeConfig, SpectrogramData } 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<string, { channelDataArrays: Float32Array[]; sampleRate: number }>();\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 = ComputeRequest | RegisterCanvasMessage | UnregisterCanvasMessage | ComputeRenderRequest | ComputeFFTRequest | RenderChunksRequest | RegisterAudioDataMessage | 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 { fftSize: actualFftSize, windowSize, frequencyBinCount, sampleRate, hopSize, frameCount, data, gainDb, rangeDb };\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 { fftSize: actualFftSize, windowSize, frequencyBinCount, sampleRate, hopSize, frameCount, data, gainDb, rangeDb };\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 { clipId, config, sampleRate: msgSampleRate, offsetSamples, durationSamples, mono, sampleRange } = msg;\n\n // Use pre-registered audio data if available, otherwise use message payload\n const registered = audioDataRegistry.get(clipId);\n const channelDataArrays = (registered && msg.channelDataArrays.length === 0)\n ? registered.channelDataArrays\n : msg.channelDataArrays;\n const sampleRate = (registered && msg.channelDataArrays.length === 0)\n ? registered.sampleRate\n : 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\n ? (sampleRange.end - sampleRange.start)\n : durationSamples;\n\n const cacheKey = generateCacheKey({\n clipId, channelIndex: 0, offsetSamples: effectiveOffset, durationSamples: effectiveDuration, 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(channelDataArrays, config, sampleRate, effectiveOffset, effectiveDuration)\n );\n } else {\n for (const channelData of channelDataArrays) {\n spectrograms.push(\n computeFromChannelData(channelData, config, sampleRate, effectiveOffset, effectiveDuration)\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 { cacheKey, canvasIds, canvasWidths, globalPixelOffsets, canvasHeight,\n devicePixelRatio, samplesPerPixel, colorLUT, frequencyScale, minFrequency,\n maxFrequency, gainDb, rangeDb, channelIndex } = 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 { channelDataArrays, config, sampleRate, offsetSamples, durationSamples, mono, render } = msg;\n\n // Compute spectrograms\n const spectrograms: SpectrogramData[] = [];\n if (mono || channelDataArrays.length === 1) {\n spectrograms.push(\n computeMonoFromChannels(channelDataArrays, config, sampleRate, offsetSamples, durationSamples)\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 } = msg as ComputeRequest;\n try {\n const spectrograms: SpectrogramData[] = [];\n\n if (mono || channelDataArrays.length === 1) {\n spectrograms.push(\n computeMonoFromChannels(channelDataArrays, config, sampleRate, offsetSamples, durationSamples)\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\nexport function getWindowFunction(\n name: string,\n size: number,\n alpha?: number\n): 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 -\n a1 * Math.cos((2 * Math.PI * i) / N) +\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': return logarithmicScale;\n case 'mel': return melScale;\n case 'bark': return barkScale;\n case 'erb': 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 { SpectrogramConfig, SpectrogramComputeConfig, SpectrogramData } 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<string, { channelDataArrays: Float32Array[]; sampleRate: number }>();\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 = ComputeRequest | RegisterCanvasMessage | UnregisterCanvasMessage | ComputeRenderRequest | ComputeFFTRequest | RenderChunksRequest | RegisterAudioDataMessage | 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 { fftSize: actualFftSize, windowSize, frequencyBinCount, sampleRate, hopSize, frameCount, data, gainDb, rangeDb };\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 { fftSize: actualFftSize, windowSize, frequencyBinCount, sampleRate, hopSize, frameCount, data, gainDb, rangeDb };\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 { clipId, config, sampleRate: msgSampleRate, offsetSamples, durationSamples, mono, sampleRange } = msg;\n\n // Use pre-registered audio data if available, otherwise use message payload\n const registered = audioDataRegistry.get(clipId);\n const channelDataArrays = (registered && msg.channelDataArrays.length === 0)\n ? registered.channelDataArrays\n : msg.channelDataArrays;\n const sampleRate = (registered && msg.channelDataArrays.length === 0)\n ? registered.sampleRate\n : 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\n ? (sampleRange.end - sampleRange.start)\n : durationSamples;\n\n const cacheKey = generateCacheKey({\n clipId, channelIndex: 0, offsetSamples: effectiveOffset, durationSamples: effectiveDuration, 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(channelDataArrays, config, sampleRate, effectiveOffset, effectiveDuration)\n );\n } else {\n for (const channelData of channelDataArrays) {\n spectrograms.push(\n computeFromChannelData(channelData, config, sampleRate, effectiveOffset, effectiveDuration)\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 { cacheKey, canvasIds, canvasWidths, globalPixelOffsets, canvasHeight,\n devicePixelRatio, samplesPerPixel, colorLUT, frequencyScale, minFrequency,\n maxFrequency, gainDb, rangeDb, channelIndex } = 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 { channelDataArrays, config, sampleRate, offsetSamples, durationSamples, mono, render } = msg;\n\n // Compute spectrograms\n const spectrograms: SpectrogramData[] = [];\n if (mono || channelDataArrays.length === 1) {\n spectrograms.push(\n computeMonoFromChannels(channelDataArrays, config, sampleRate, offsetSamples, durationSamples)\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 } = msg as ComputeRequest;\n try {\n const spectrograms: SpectrogramData[] = [];\n\n if (mono || channelDataArrays.length === 1) {\n spectrograms.push(\n computeMonoFromChannels(channelDataArrays, config, sampleRate, offsetSamples, durationSamples)\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"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@waveform-playlist/spectrogram",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.0",
|
|
4
4
|
"description": "Spectrogram computation and UI for waveform-playlist",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -43,16 +43,16 @@
|
|
|
43
43
|
"@types/styled-components": "^5.1.26",
|
|
44
44
|
"tsup": "^8.0.1",
|
|
45
45
|
"typescript": "^5.3.3",
|
|
46
|
-
"@waveform-playlist/browser": "
|
|
46
|
+
"@waveform-playlist/browser": "7.0.0"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"fft.js": "^4.0.4",
|
|
50
|
-
"@waveform-playlist/core": "
|
|
50
|
+
"@waveform-playlist/core": "7.0.0"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
53
|
"react": "^18.0.0",
|
|
54
54
|
"styled-components": "^6.0.0",
|
|
55
|
-
"@waveform-playlist/browser": "
|
|
55
|
+
"@waveform-playlist/browser": "7.0.0"
|
|
56
56
|
},
|
|
57
57
|
"scripts": {
|
|
58
58
|
"build": "tsup",
|