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
+21 -112
View File
@@ -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();
}