// workers/waveform.worker.js — Renders min/max waveform data on OffscreenCanvas const FRAME_INTERVAL_MS = 1000 / 48; const DEFAULT_COLORS = { leftColor: '#00e7ff', rightColor: '#ff6b81', diffColor: '#00e7ff', }; const MODE_STACKED = 'stacked'; const MODE_OVERLAY = 'overlay'; const MODE_DIFF = 'diff'; const state = { canvas: null, ctx: null, width: 0, height: 0, mode: MODE_STACKED, colors: { ...DEFAULT_COLORS }, pending: null, lastDraw: 0, timer: null, }; self.onmessage = (event) => { const data = event.data || {}; switch (data.type) { case 'init': handleInit(data); break; case 'resize': handleResize(data); break; case 'config': handleConfig(data); break; case 'data': handleData(data); break; case 'dispose': dispose(); break; default: break; } }; function handleInit({ canvas, width, height, mode, colors }) { if (!canvas) return; state.canvas = canvas; state.ctx = canvas.getContext('2d', { alpha: true, desynchronized: true }); state.mode = normalizeMode(mode); state.colors = normalizeColors(colors); handleResize({ width, height }); postMessage({ type: 'ready' }); } function handleResize({ width, height }) { if (!state.canvas || !Number.isFinite(width) || !Number.isFinite(height)) return; state.canvas.width = Math.max(1, Math.floor(width)); state.canvas.height = Math.max(1, Math.floor(height)); state.width = state.canvas.width; state.height = state.canvas.height; } function handleConfig({ mode, colors }) { state.mode = normalizeMode(mode); state.colors = normalizeColors(colors); } function handleData({ minMaxL, minMaxR, channelCount }) { state.pending = { minMaxL, minMaxR, channelCount: channelCount || (minMaxR ? 2 : 1), }; scheduleDraw(); } function scheduleDraw() { if (!state.ctx || !state.pending) return; const now = performance.now(); const delta = now - state.lastDraw; if (delta >= FRAME_INTERVAL_MS) { drawFrame(); state.lastDraw = now; } else if (!state.timer) { state.timer = setTimeout(() => { state.timer = null; drawFrame(); state.lastDraw = performance.now(); }, FRAME_INTERVAL_MS - delta); } } function drawFrame() { if (!state.ctx || !state.pending) return; const { minMaxL, minMaxR, channelCount } = state.pending; state.pending = null; state.ctx.clearRect(0, 0, state.width, state.height); const rects = getChannelRects(state.height, channelCount, state.mode); if (state.mode === MODE_DIFF) { const diffData = buildDiffData(minMaxL, minMaxR); if (diffData) { drawChannel(state.ctx, diffData, rects[0], state.colors.diffColor); } return; } if (state.mode === MODE_STACKED && channelCount > 1 && rects.length >= 2) { drawChannel(state.ctx, minMaxL, rects[0], state.colors.leftColor); drawChannel(state.ctx, minMaxR || minMaxL, rects[1], state.colors.rightColor); } else { drawChannel(state.ctx, minMaxL, rects[0], state.colors.leftColor); if (channelCount > 1 && minMaxR) { drawChannel(state.ctx, minMaxR, rects[0], state.colors.rightColor, 0.75); } } } function normalizeColors(colors) { return { leftColor: colors?.leftColor || DEFAULT_COLORS.leftColor, rightColor: colors?.rightColor || DEFAULT_COLORS.rightColor, diffColor: colors?.diffColor || DEFAULT_COLORS.diffColor, }; } function drawChannel(ctx, data, rect, color, alpha = 1) { if (!data || !data.length) return; const width = data.length / 2; const scaleX = width ? state.width / width : 1; ctx.save(); ctx.strokeStyle = color; ctx.globalAlpha = alpha; ctx.lineWidth = 1; ctx.beginPath(); for (let x = 0; x < width; x++) { const idx = x * 2; const min = clampAmp(data[idx]); const max = clampAmp(data[idx + 1]); const xPos = x * scaleX + 0.5; const yMax = ampToY(max, rect.y, rect.h); const yMin = ampToY(min, rect.y, rect.h); ctx.moveTo(xPos, yMax); ctx.lineTo(xPos, yMin); } ctx.stroke(); ctx.restore(); } function getChannelRects(totalHeight, channelCount, mode) { if (mode === MODE_DIFF) { return [{ y: 0, h: totalHeight }]; } if (mode === MODE_STACKED && channelCount > 1) { const gap = 6; const half = (totalHeight - gap) / 2; return [ { y: 0, h: half }, { y: half + gap, h: half }, ]; } return [{ y: 0, h: totalHeight }]; } function ampToY(value, y, h) { const v = clampAmp(value); return y + (1 - ((v + 1) * 0.5)) * h; } function clampAmp(value) { if (!Number.isFinite(value)) return 0; return Math.max(-1, Math.min(1, value)); } function normalizeMode(mode) { if (mode === MODE_OVERLAY) return MODE_OVERLAY; if (mode === MODE_DIFF) return MODE_DIFF; return MODE_STACKED; } function buildDiffData(minMaxL, minMaxR) { if (!minMaxL || !minMaxR) return null; const pairs = Math.min(minMaxL.length, minMaxR.length) / 2; if (!pairs) return null; const out = new Float32Array(pairs * 2); for (let i = 0; i < pairs; i++) { const idx = i * 2; const lMin = clampAmp(minMaxL[idx]); const lMax = clampAmp(minMaxL[idx + 1]); const rMin = clampAmp(minMaxR[idx]); const rMax = clampAmp(minMaxR[idx + 1]); // Korrigierte Differenz-Berechnung const diffMin = lMin - rMin; const diffMax = lMax - rMax; out[idx] = clampAmp(Math.min(diffMin, diffMax)); out[idx + 1] = clampAmp(Math.max(diffMin, diffMax)); } return out; } function dispose() { if (state.timer) { clearTimeout(state.timer); state.timer = null; } state.pending = null; state.canvas = null; state.ctx = null; }