213 lines
5.5 KiB
JavaScript
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;
|
|
}
|