Files
Phoenix/www/workers/waveform.worker.js
2026-06-02 20:56:19 +02:00

213 lines
5.5 KiB
JavaScript

// 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;
}