Recover analyzer DSP and realtime pipeline corrections
This commit is contained in:
+21
-112
@@ -16,8 +16,6 @@ const METER_SLOT_SHRINK = 24;
|
||||
const METER_PAD_TOP = 1;
|
||||
const METER_PAD_BOTTOM = -7;
|
||||
const METER_EXTRA_BOTTOM_PAD = 6;
|
||||
const CORR_ATTACK_S = 1.5;
|
||||
const CORR_RELEASE_S = 2.5;
|
||||
const GONIO_GAIN_MIN_DB = -35;
|
||||
const GONIO_GAIN_MAX_DB = 35;
|
||||
const BASE_TARGET = 1.0;
|
||||
@@ -26,8 +24,7 @@ const BASE_ZOOM = 1.0;
|
||||
const BASE_GONIO_SCALE = (BASE_TARGET / ALIGN_PEAK) * Math.pow(10, -BASE_HEADROOM_DB / 20) * BASE_ZOOM;
|
||||
const AGC_TARGET_DB = linearToDb(ALIGN_PEAK);
|
||||
const CORR_SILENCE_THRESHOLD_DEFAULT = -75;
|
||||
const CORR_SILENCE_HOLD_MS = 30000;
|
||||
const CORR_SILENCE_DRIFT_MS = 60000;
|
||||
const MAX_TRAIL_FRAMES = 48;
|
||||
|
||||
function computePanelSlotWidth(canvasWidth, slotCount = METER_SLOTS, slotGap = 12) {
|
||||
const n = Math.max(1, Math.min(METER_SLOTS, slotCount | 0));
|
||||
@@ -39,11 +36,6 @@ function computePanelSlotWidth(canvasWidth, slotCount = METER_SLOTS, slotGap = 1
|
||||
|
||||
export function init() {
|
||||
return {
|
||||
corrDisplayed: 0,
|
||||
corrMeter: 0,
|
||||
corrLastTs: 0,
|
||||
corrLastValid: 0,
|
||||
corrSilenceSince: 0,
|
||||
gateLevel: 1,
|
||||
gateLastTs: 0,
|
||||
agcEnv: 1e-3,
|
||||
@@ -51,6 +43,7 @@ export function init() {
|
||||
agcLastTs: 0,
|
||||
traceBuffer: new Float32Array(0),
|
||||
lineTrails: [],
|
||||
trailPool: [],
|
||||
staticLayerCanvas: null,
|
||||
staticLayerCtx: null,
|
||||
staticLayerKey: '',
|
||||
@@ -66,13 +59,7 @@ export async function render(env, state) {
|
||||
|
||||
const wakeTs = Number.isFinite(env?.screensaverWakeTs) ? Number(env.screensaverWakeTs) : 0;
|
||||
if (wakeTs && state?._lastWakeTs !== wakeTs) {
|
||||
// Nach Screensaver-Wake: Korrelation sauber zurücksetzen, damit kein "alter" Wert (z.B. +1) stehen bleibt.
|
||||
state._lastWakeTs = wakeTs;
|
||||
state.corrDisplayed = 0;
|
||||
state.corrMeter = 0;
|
||||
state.corrLastTs = 0;
|
||||
state.corrLastValid = 0;
|
||||
state.corrSilenceSince = frameNow;
|
||||
}
|
||||
|
||||
const slotsRaw = env.slots?.(id) || ['vu', 'ppm-ebu', 'tp'];
|
||||
@@ -101,62 +88,20 @@ export async function render(env, state) {
|
||||
: null;
|
||||
renderTrace(g, state, trace, layout.scope, style, xyReady && gate.active, settings);
|
||||
|
||||
const silenceGateEnabled = CONFIG?.XY_SILENCE_GATE_ENABLED !== false;
|
||||
const corrThreshold = resolveCorrThreshold(CONFIG);
|
||||
const rmsL = Number.isFinite(env.audio?.rmsDb?.L) ? env.audio.rmsDb.L : -120;
|
||||
const rmsR = Number.isFinite(env.audio?.rmsDb?.R) ? env.audio.rmsDb.R : -120;
|
||||
const activeL = audioFresh && (!silenceGateEnabled || (rmsL >= corrThreshold));
|
||||
const activeR = audioFresh && (!silenceGateEnabled || (rmsR >= corrThreshold));
|
||||
|
||||
let corrTarget = 0;
|
||||
const canCorr = xyReady && utils?.correlation;
|
||||
const zeroOnSilence = !!CONFIG?.CORR_ZERO_ON_SILENCE;
|
||||
const bothSilent = !activeL && !activeR;
|
||||
|
||||
if (activeL && activeR && canCorr) {
|
||||
// Beide Kanäle aktiv → echte Korrelation aus L/R-Samples
|
||||
const corrRaw = utils.correlation(
|
||||
xyData.xyL,
|
||||
xyData.xyR,
|
||||
Number.isFinite(state.corrDisplayed) ? state.corrDisplayed : 0,
|
||||
CONFIG.CORR_SMOOTH,
|
||||
);
|
||||
if (Number.isFinite(corrRaw)) {
|
||||
state.corrDisplayed = corrRaw;
|
||||
state.corrLastValid = corrRaw;
|
||||
state.corrSilenceSince = 0;
|
||||
corrTarget = corrRaw;
|
||||
}
|
||||
} else if (activeL !== activeR) {
|
||||
// Nur ein Kanal aktiv → hart auf 0, keine Hold-/Decay-Logik
|
||||
state.corrSilenceSince = 0;
|
||||
corrTarget = 0;
|
||||
} else {
|
||||
if (zeroOnSilence && bothSilent) {
|
||||
// Beide still → zügig Richtung 0 (ohne Hold/Drift)
|
||||
state.corrLastValid = 0;
|
||||
state.corrSilenceSince = frameNow;
|
||||
corrTarget = 0;
|
||||
} else {
|
||||
// Beide still → Hold + Drift zur Mitte
|
||||
if (!state.corrSilenceSince) state.corrSilenceSince = frameNow;
|
||||
corrTarget = resolveCorrTarget(state, frameNow);
|
||||
}
|
||||
}
|
||||
|
||||
const fastZero = zeroOnSilence && bothSilent;
|
||||
const corrVisual = updateCorrelationDisplay(state, corrTarget, frameNow, fastZero ? true : gate.active);
|
||||
if (fastZero) {
|
||||
// Damit die nächste echte Korrelation nicht noch an einem alten Glättungswert "hängt".
|
||||
state.corrDisplayed = corrVisual;
|
||||
}
|
||||
// Correlation is measured continuously in the audio DSP. Do not add a
|
||||
// second, frame-rate-dependent browser integration here.
|
||||
const corrVisual = Number.isFinite(audio?.correlation) ? clamp1(audio.correlation) : 0;
|
||||
const corrNegativePeak = Number.isFinite(audio?.correlationNegativePeak)
|
||||
? clamp1(audio.correlationNegativePeak)
|
||||
: 0;
|
||||
drawCorrelationBar(
|
||||
g,
|
||||
layout.plot.x + layout.plot.w / 2,
|
||||
layout.plot.y + layout.plot.h - 28,
|
||||
Math.round(Math.min(layout.scope.w * 0.75, layout.plot.w - 50)),
|
||||
18,
|
||||
corrVisual
|
||||
corrVisual,
|
||||
corrNegativePeak,
|
||||
);
|
||||
|
||||
if (slots.length) {
|
||||
@@ -755,52 +700,6 @@ function getNow() {
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
function updateCorrelationDisplay(state, target, nowTs, gateActive = true) {
|
||||
const prev = Number.isFinite(state.corrMeter) ? state.corrMeter : target;
|
||||
const lastTs = Number.isFinite(state.corrLastTs) ? state.corrLastTs : nowTs;
|
||||
const dt = Math.max(0, (nowTs - lastTs) / 1000);
|
||||
if (!dt) {
|
||||
state.corrMeter = clamp1(target);
|
||||
state.corrLastTs = nowTs;
|
||||
return state.corrMeter;
|
||||
}
|
||||
const rising = Math.abs(target) > Math.abs(prev);
|
||||
// Schneller einrasten, wenn Gate aktiv ist; etwas zäher auf Null auslaufen, wenn Stille.
|
||||
const tauAttack = gateActive ? 0.08 : CORR_ATTACK_S;
|
||||
const tauRelease = gateActive ? 0.35 : CORR_RELEASE_S;
|
||||
const tau = rising ? tauAttack : tauRelease;
|
||||
const alpha = 1 - Math.exp(-dt / Math.max(0.001, tau));
|
||||
const next = clamp1(prev + (target - prev) * alpha);
|
||||
state.corrMeter = next;
|
||||
state.corrLastTs = nowTs;
|
||||
return next;
|
||||
}
|
||||
|
||||
function resolveCorrThreshold(CONFIG = {}) {
|
||||
if (Number.isFinite(CONFIG.XY_SILENCE_THRESHOLD_RMS_DBFS)) {
|
||||
return CONFIG.XY_SILENCE_THRESHOLD_RMS_DBFS;
|
||||
}
|
||||
if (Number.isFinite(CONFIG.CORR_SILENCE_THRESHOLD_RMS_DBFS)) {
|
||||
return CONFIG.CORR_SILENCE_THRESHOLD_RMS_DBFS;
|
||||
}
|
||||
return CORR_SILENCE_THRESHOLD_DEFAULT;
|
||||
}
|
||||
|
||||
function resolveCorrTarget(state, nowTs) {
|
||||
const lastValid = Number.isFinite(state.corrLastValid) ? state.corrLastValid : 0;
|
||||
if (!state.corrSilenceSince) {
|
||||
return lastValid;
|
||||
}
|
||||
const holdMs = CORR_SILENCE_HOLD_MS;
|
||||
const driftMs = CORR_SILENCE_DRIFT_MS;
|
||||
const elapsed = Math.max(0, nowTs - state.corrSilenceSince);
|
||||
if (elapsed <= holdMs) {
|
||||
return lastValid;
|
||||
}
|
||||
const t = Math.min(1, (elapsed - holdMs) / Math.max(1, driftMs));
|
||||
return lastValid * (1 - t);
|
||||
}
|
||||
|
||||
function computeAgcGain(state, xyData, nowTs) {
|
||||
const attackTau = 0.001; // 1 ms
|
||||
const releaseDbPerS = 10;
|
||||
@@ -857,7 +756,7 @@ function dbToLinear(db) {
|
||||
// -----------------------------------------------------------------------------
|
||||
// Correlation bar helper
|
||||
|
||||
function drawCorrelationBar(g, centerX, y, w, h, val) {
|
||||
function drawCorrelationBar(g, centerX, y, w, h, val, negativePeak) {
|
||||
const x = Math.floor(centerX - w / 2);
|
||||
g.save();
|
||||
g.strokeStyle = FRAME_COLOR; g.lineWidth = 2; g.strokeRect(x, y, w, h);
|
||||
@@ -886,6 +785,16 @@ function drawCorrelationBar(g, centerX, y, w, h, val) {
|
||||
g.lineWidth = 1;
|
||||
g.strokeRect(cubeX, cubeY, cubeSize, cubeSize);
|
||||
g.beginPath(); g.moveTo(mid, y); g.lineTo(mid, y + h); g.stroke();
|
||||
if (Number.isFinite(negativePeak) && negativePeak < 0.999) {
|
||||
const markerX = clampCenter(mid + (clamp1(negativePeak) * 0.5) * w);
|
||||
g.fillStyle = WARN_COLOR;
|
||||
g.beginPath();
|
||||
g.moveTo(markerX, y - 1);
|
||||
g.lineTo(markerX - 5, y - 7);
|
||||
g.lineTo(markerX + 5, y - 7);
|
||||
g.closePath();
|
||||
g.fill();
|
||||
}
|
||||
g.restore();
|
||||
}
|
||||
|
||||
|
||||
+10
-8
@@ -71,14 +71,15 @@ export async function render(env, state) {
|
||||
const nyq = audio.nyq || 24000;
|
||||
const analyser = audio.getAnalyser?.();
|
||||
const buf = audio.getFreqBuffer?.();
|
||||
const useIirEngine = (CONFIG.RTA_ENGINE || 'fft') === 'iir';
|
||||
const rtaData = typeof audio.getRtaData === 'function'
|
||||
? audio.getRtaData()
|
||||
: null;
|
||||
const useIirEngine = rtaData?.engine === 'iir'
|
||||
|| ((CONFIG.RTA_ENGINE || 'fft') === 'iir');
|
||||
const useNativeFftEngine = !useIirEngine && rtaData && rtaData.engine === 'fft';
|
||||
const nativeRtaPacket = (useIirEngine || useNativeFftEngine) ? rtaData : null;
|
||||
const displayRtaPacket = (nativeRtaPacket && CONFIG.RTA_BAR_LAYOUT === 'rtw')
|
||||
? selectLocalRtwPacket(nativeRtaPacket, CONFIG.RTA_BPO_MODE || '1_6')
|
||||
? selectLocalRtwPacket(nativeRtaPacket, nativeRtaPacket.bpo || '1_3')
|
||||
: nativeRtaPacket;
|
||||
|
||||
const topInset = Number.isFinite(env?.topInset) ? Number(env.topInset) : DEFAULT_TOP_INSET;
|
||||
@@ -177,9 +178,9 @@ export async function render(env, state) {
|
||||
} else {
|
||||
const integrated = baseLevels;
|
||||
const displayBase = applyDisplayHold(state, integrated, CONFIG, range);
|
||||
const display = (CONFIG.REALTIME_RENDER_STYLE || 'bars') === 'bars'
|
||||
? applyRealtimeBarBallistics(state, displayBase, CONFIG, range)
|
||||
: displayBase;
|
||||
// Native IIR values already contain the selected power-domain
|
||||
// integration. A second browser attack/hold stage would falsify it.
|
||||
const display = displayBase;
|
||||
applyPeakHold(state, integrated, CONFIG, range);
|
||||
state.displayLevels = display;
|
||||
state.currentRange = range;
|
||||
@@ -320,10 +321,11 @@ function resetState(state) {
|
||||
|
||||
function ensureBands(state, utils, CONFIG, nyq, binCount, freqBounds, range, rtaPacket) {
|
||||
const freqRange = CONFIG.RTA_FREQ_RANGE === 'lf' ? 'lf' : 'norm';
|
||||
const bpo = CONFIG.RTA_BPO_MODE || '1_6';
|
||||
const engine = CONFIG.RTA_ENGINE || 'fft';
|
||||
const layoutMode = CONFIG.RTA_BAR_LAYOUT === 'rtw' ? 'rtw' : 'iec';
|
||||
const rtaCenters = (layoutMode === 'rtw') ? getRtwCenters(bpo) : null;
|
||||
const bpo = layoutMode === 'rtw' ? (rtaPacket?.bpo || '1_3') : (CONFIG.RTA_BPO_MODE || '1_6');
|
||||
const engine = rtaPacket?.engine || CONFIG.RTA_ENGINE || 'fft';
|
||||
const packetCenters = isVectorLike(rtaPacket?.centers) ? Array.from(rtaPacket.centers) : null;
|
||||
const rtaCenters = (layoutMode === 'rtw') ? (packetCenters || getRtwCenters(bpo)) : null;
|
||||
const centerKey = rtaCenters
|
||||
? `${rtaCenters.length}:${rtaCenters[0]}:${rtaCenters[rtaCenters.length - 1]}`
|
||||
: 'static';
|
||||
|
||||
+124
-33
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user