Initial Phoenix analyzer
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
// 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;
|
||||
}
|
||||
Reference in New Issue
Block a user