Recover analyzer DSP and realtime pipeline corrections

This commit is contained in:
Mikei386
2026-07-21 19:57:52 +02:00
parent e499bac928
commit 91aeccb938
27 changed files with 3410 additions and 924 deletions
+124 -33
View File
@@ -15,6 +15,9 @@ const SMOOTHING_MAX = 0.2;
const TIME_GRID_SPACING = 100;
const SPECTROGRAM_ID = 'spectrogram';
const SPECTRO_RENDER_SCALE = 0.85;
const BASE_SCROLL_CSS_PX_PER_SECOND = 60;
const WORKER_WATCHDOG_MS = 750;
const MAX_SCROLL_COLUMNS_PER_DRAW = 64;
const SUPPORTS_WORKER = typeof window !== 'undefined'
&& typeof Worker !== 'undefined'
&& typeof HTMLCanvasElement !== 'undefined'
@@ -38,6 +41,13 @@ export function init() {
useWorker: false, // wird im Render je nach Config gesetzt
worker: null,
workerReady: false,
workerBusy: false,
workerMessageId: 0,
workerWatchdog: null,
pendingWorkerColumn: null,
pendingWorkerRepeat: 0,
pendingWorkerDropped: 0,
workerStats: { sent: 0, drawn: 0, dropped: 0, restarts: 0, lastDrawMs: 0 },
offscreenWidth: 0,
offscreenHeight: 0,
spectroCanvasEl: null,
@@ -93,6 +103,10 @@ export async function render(env, state) {
}
// Config-Flag schaltet Worker ein (default true), wenn Offscreen verfügbar
state.useWorker = SUPPORTS_WORKER && !!env.config?.SPECTRO_USE_WORKER;
if (!state.useWorker && state.worker) {
teardownWorker(state);
removeSpectroCanvasElement(state);
}
state._dbgNextLog = state._dbgNextLog || 0;
const { ctx: g, rect, config: CONFIG, audio, meters } = env;
@@ -165,6 +179,7 @@ export async function render(env, state) {
}
const scrollRate = resolveScrollRate(CONFIG);
const outputPixelScale = state.useWorker ? (plotWpx / Math.max(1, plotW)) : 1;
let spectroDirty = true;
if (hasPhoenixSpectro) {
spectroDirty = phoenixSpectroSeq !== (state.lastPhoenixSpectroSeq || 0);
@@ -173,15 +188,19 @@ export async function render(env, state) {
let columnsToEmit = 0;
if (hasPhoenixSpectro) {
if (spectroDirty) {
state.scrollAccumulator = (state.scrollAccumulator || 0) + scrollRate;
while (state.scrollAccumulator >= 1) {
columnsToEmit += 1;
state.scrollAccumulator -= 1;
}
if (columnsToEmit < 1) {
columnsToEmit = 1;
state.scrollAccumulator = 0;
}
const previousSeq = Number(state.lastPhoenixSpectroSeq || 0);
const sequenceDelta = previousSeq > 0
? Math.max(1, phoenixSpectroSeq - previousSeq)
: 1;
const fftSize = Number(env.audio?.phoenixSpectroMeta?.fftSize) || freqBuf.length * 2;
const pixelsPerSourceFrame = spectrogramPixelsPerSourceFrame(scrollRate, fs, fftSize);
const scrollStep = stepScrollAccumulator(
state.scrollAccumulator,
pixelsPerSourceFrame * outputPixelScale,
sequenceDelta,
);
state.scrollAccumulator = scrollStep.accumulator;
columnsToEmit = scrollStep.columns;
}
} else {
const nowTs = performance.now();
@@ -206,15 +225,11 @@ export async function render(env, state) {
if (columnsToEmit > 0 && spectroDirty) {
const column = buildColumnData(state, freqBuf, range.bottom);
const columns = [];
for (let i = 0; i < columnsToEmit; i++) {
columns.push(i === 0 ? column : new Float32Array(column));
}
if (state.useWorker && state.workerReady) {
columns.forEach(col => sendColumnToWorker(state, col));
if (state.useWorker) {
queueColumnForWorker(state, column, columnsToEmit);
} else {
columns.forEach(col => writeColumnToHistory(state, col));
const repeat = Math.min(state.width, columnsToEmit);
for (let index = 0; index < repeat; index++) writeColumnToHistory(state, column);
}
}
@@ -249,7 +264,7 @@ function setupWorker(state, widthPx, heightPx, range, gamma, freqBounds) {
worker.onmessage = (event) => handleWorkerMessage(state, event);
worker.onerror = () => {
state.useWorker = false;
teardownWorker(state);
restartWorker(state);
};
const offscreen = canvasEl.transferControlToOffscreen();
state.spectroCanvasEl = canvasEl;
@@ -310,6 +325,7 @@ function setupWorker(state, widthPx, heightPx, range, gamma, freqBounds) {
}
function teardownWorker(state) {
clearWorkerWatchdog(state);
if (state.worker) {
try {
state.worker.postMessage({ type: 'dispose' });
@@ -320,39 +336,97 @@ function teardownWorker(state) {
}
state.worker = null;
state.workerReady = false;
state.spectroCanvasEl = null;
state.spectroCanvasTransferred = false;
state.workerBusy = false;
state.pendingWorkerColumn = null;
state.pendingWorkerRepeat = 0;
state.pendingWorkerDropped = 0;
}
function handleWorkerMessage(state, event) {
const data = event.data || {};
if (data.type === 'ready') {
state.workerReady = true;
state.workerBusy = false;
pumpWorkerColumn(state);
} else if (data.type === 'drawn') {
if (Number(data.id) !== Number(state.workerMessageId)) return;
clearWorkerWatchdog(state);
state.workerBusy = false;
state.workerStats.drawn += Math.max(0, Number(data.repeat) || 0);
state.workerStats.lastDrawMs = Math.max(0, Number(data.drawMs) || 0);
pumpWorkerColumn(state);
} else if (data.type === 'error') {
restartWorker(state);
} else if (data.type === 'dbg' && state._dbgEnabled) {
console.debug('[spectro worker]', data);
}
}
function sendColumnToWorker(state, column) {
if (!state.worker || !column) return;
// Prüfe ob Worker bereit ist und nicht überlastet
if (!state.workerReady) return;
try {
state.worker.postMessage({ type: 'column', data: column }, [column.buffer]);
} catch (e) {
state.useWorker = false;
function queueColumnForWorker(state, column, repeat = 1) {
if (!column) return;
const requested = Math.max(1, Math.floor(Number(repeat) || 1));
if (state.pendingWorkerColumn) {
state.pendingWorkerDropped += 1;
state.workerStats.dropped += 1;
}
advanceWriteIndex(state);
state.pendingWorkerColumn = column;
state.pendingWorkerRepeat = Math.min(
Math.max(1, state.offscreenWidth || state.width || 1),
Math.max(0, state.pendingWorkerRepeat) + requested,
);
pumpWorkerColumn(state);
}
function pumpWorkerColumn(state) {
if (!state.worker || !state.workerReady || state.workerBusy || !state.pendingWorkerColumn) return;
const column = state.pendingWorkerColumn;
const repeat = Math.min(MAX_SCROLL_COLUMNS_PER_DRAW, Math.max(1, state.pendingWorkerRepeat || 1));
const dropped = Math.max(0, state.pendingWorkerDropped || 0);
state.pendingWorkerColumn = null;
// A large value means the display was suspended or overloaded. Never catch
// up for seconds; jump by a bounded amount and continue with live data.
state.pendingWorkerRepeat = 0;
state.pendingWorkerDropped = 0;
state.workerBusy = true;
state.workerMessageId += 1;
const id = state.workerMessageId;
try {
state.worker.postMessage({ type: 'column', id, data: column, repeat, dropped }, [column.buffer]);
state.workerStats.sent += repeat;
} catch (e) {
restartWorker(state);
return;
}
clearWorkerWatchdog(state);
state.workerWatchdog = setTimeout(() => {
if (!state.workerBusy || id !== state.workerMessageId) return;
restartWorker(state);
}, WORKER_WATCHDOG_MS);
// Debug: alle ~1s senden wir ein Status-Log
if (state._dbgEnabled && performance.now() >= state._dbgNextLog) {
state._dbgNextLog = performance.now() + 1000;
console.debug('[spectro main] sent column', { writeIndex: state.writeIndex });
console.debug('[spectro]', { ...state.workerStats, busy: state.workerBusy, queuedRepeat: state.pendingWorkerRepeat });
}
}
function clearWorkerWatchdog(state) {
if (!state.workerWatchdog) return;
clearTimeout(state.workerWatchdog);
state.workerWatchdog = null;
}
function restartWorker(state) {
clearWorkerWatchdog(state);
if (state.worker) {
try { state.worker.terminate(); } catch (_) {}
}
state.worker = null;
state.workerReady = false;
state.workerBusy = false;
state.workerStats.restarts += 1;
removeSpectroCanvasElement(state);
}
function ensureFallbackBuffers(state, width, height, fillDb, g) {
const needsInit = !state.history ||
state.width !== width ||
@@ -527,6 +601,23 @@ function resolveScrollRate(CONFIG) {
return 1;
}
export function stepScrollAccumulator(accumulator, rate, frameDelta = 1) {
const safeAccumulator = Number.isFinite(Number(accumulator)) ? Math.max(0, Number(accumulator)) : 0;
const safeRate = Number.isFinite(Number(rate)) ? Math.max(0.01, Math.min(64, Number(rate))) : 1;
const safeDelta = Number.isFinite(Number(frameDelta)) ? Math.max(1, Math.floor(Number(frameDelta))) : 1;
const total = safeAccumulator + safeRate * safeDelta;
const columns = Math.floor(total + 1e-9);
return { columns, accumulator: Math.max(0, total - columns) };
}
export function spectrogramPixelsPerSourceFrame(rate, sampleRate, fftSize) {
const safeRate = [0.5, 1, 2, 4, 6].includes(Number(rate)) ? Number(rate) : 1;
const safeSampleRate = Math.max(8000, Number(sampleRate) || 48000);
const safeFftSize = Math.max(1024, Math.floor(Number(fftSize) || 4096));
const hopSamples = Math.max(128, Math.floor(safeFftSize / 8));
return BASE_SCROLL_CSS_PX_PER_SECOND * safeRate * hopSamples / safeSampleRate;
}
function advanceWriteIndex(state) {
const width = Math.max(1, state.width || 1);
state.writeIndex = (state.writeIndex + 1) % width;