Correct goniometer transport and correlation meter

This commit is contained in:
Mikei386
2026-07-21 20:06:47 +02:00
parent 91aeccb938
commit 54f0f7a450
12 changed files with 297 additions and 80 deletions
+95 -31
View File
@@ -41,6 +41,7 @@ export function init() {
agcEnv: 1e-3,
agcGainDb: 0,
agcLastTs: 0,
agcLastXySeq: 0,
traceBuffer: new Float32Array(0),
lineTrails: [],
trailPool: [],
@@ -51,7 +52,12 @@ export function init() {
}
export function resize() {}
export function destroy() {}
export function destroy(state) {
if (!state) return;
clearTrails(state);
state.trailPool.length = 0;
state.traceBuffer = new Float32Array(0);
}
export async function render(env, state) {
const { ctx: g, rect, config: CONFIG, audio, utils, meters } = env;
@@ -68,12 +74,14 @@ export async function render(env, state) {
const layout = computeGoniometerLayout(rect, CONFIG, slots.length, topInset);
const settings = resolveScopeSettings(CONFIG);
const style = CONFIG.XY_STYLE === 'points' ? 'points' : 'lines';
const gate = resolveSilenceGate(env, CONFIG, state);
drawStaticLayer(g, state, rect, layout, CONFIG, slots.length);
const xyData = extractXYData(audio);
const lastSampleTs = Number.isFinite(env?.audio?.lastSampleTs) ? Number(env.audio.lastSampleTs) : 0;
const gate = resolveSilenceGate(CONFIG, state, xyData);
const lastSampleTs = Number.isFinite(env?.audio?.xyLastSampleTs)
? Number(env.audio.xyLastSampleTs)
: 0;
const audioAgeMs = lastSampleTs ? (frameNow - lastSampleTs) : Infinity;
const audioFresh = Number.isFinite(audioAgeMs) && audioAgeMs >= 0 && audioAgeMs <= 250;
const xyReady = xyData.ready && audioFresh;
@@ -396,6 +404,7 @@ function extractXYData(audio) {
ready,
xyL,
xyR,
seq: Number(audio?.xySeq) || 0,
length: ready ? Math.min(xyL.length, xyR.length) : 0,
};
}
@@ -403,7 +412,7 @@ function extractXYData(audio) {
function resolveScopeSettings(CONFIG) {
let gainDb = Number.isFinite(CONFIG?.GONIO_DISPLAY_GAIN_DB) ? CONFIG.GONIO_DISPLAY_GAIN_DB : 0;
gainDb = Math.max(GONIO_GAIN_MIN_DB, Math.min(GONIO_GAIN_MAX_DB, Math.round(gainDb / 5) * 5));
const lineFadeMs = Number.isFinite(CONFIG?.GONIO_LINE_FADE_MS) ? CONFIG.GONIO_LINE_FADE_MS : 300;
const lineFadeMs = resolveGoniometerPersistenceMs(CONFIG);
const rtwClassic = true;
return {
gainDb,
@@ -417,16 +426,37 @@ function resolveScopeSettings(CONFIG) {
pointBaseAlpha: rtwClassic ? 0.14 : 0.16,
pointBoostAlpha: rtwClassic ? 0.34 : 0.36,
pointSize: rtwClassic ? 1.4 : 2.2,
curveSmoothing: rtwClassic,
// Do not bend the measured M/S sample path with cosmetic Bézier curves.
curveSmoothing: false,
};
}
function resolveSilenceGate(env, CONFIG, state) {
export function resolveGoniometerPersistenceMs(CONFIG = {}) {
const mode = String(CONFIG.GONIO_PERSISTENCE_MODE || 'fast').toLowerCase();
if (mode === 'medium') return 150;
if (mode === 'slow') return 300;
if (mode === 'custom') {
const custom = Number(CONFIG.GONIO_LINE_FADE_MS);
return Number.isFinite(custom) ? Math.max(0, Math.min(600, custom)) : 50;
}
return 50;
}
function resolveSilenceGate(CONFIG, state, xyData) {
const enabled = CONFIG?.XY_SILENCE_GATE_ENABLED !== false;
const threshold = Number.isFinite(CONFIG?.XY_SILENCE_THRESHOLD_RMS_DBFS)
? CONFIG.XY_SILENCE_THRESHOLD_RMS_DBFS
: CORR_SILENCE_THRESHOLD_DEFAULT;
const rmsMono = Number.isFinite(env.audio?.rmsDb?.mono) ? env.audio.rmsDb.mono : -120;
let power = 0;
if (xyData?.ready && xyData.length > 0) {
for (let index = 0; index < xyData.length; index++) {
const left = Number(xyData.xyL[index]) || 0;
const right = Number(xyData.xyR[index]) || 0;
power += left * left + right * right;
}
power /= 2 * xyData.length;
}
const rmsMono = power > 1e-12 ? 10 * Math.log10(power) : -120;
const targetActive = enabled ? (rmsMono >= threshold) : true;
const now = getNow();
const dt = state.gateLastTs ? Math.max(0, (now - state.gateLastTs) / 1000) : 0;
@@ -450,7 +480,8 @@ function buildTrace(state, xyData, scope, scale, CONFIG) {
if (targetPoints <= 1 || total <= 1) {
return { buffer: null, count: 0 };
}
const sampleCount = Math.max(2, Math.min(targetPoints, MAX_TRACE_POINTS));
// The browser may reduce density but must never invent interpolated samples.
const sampleCount = Math.max(2, Math.min(targetPoints, total, MAX_TRACE_POINTS));
if (sampleCount <= 1) {
return { buffer: null, count: 0 };
}
@@ -497,7 +528,7 @@ function renderTrace(g, state, trace, scope, style, xyReady, settings) {
if (style === 'lines') {
if (lineFadeMs <= 0) {
trails.length = 0;
clearTrails(state);
if (hasTrace && trace.count > 1) {
drawImmediateLine(g, trace, scope, settings);
} else if (!xyReady) {
@@ -505,14 +536,14 @@ function renderTrace(g, state, trace, scope, style, xyReady, settings) {
}
} else {
if (hasTrace && trace.count > 1) {
addLineTrail(trails, trace, now);
addLineTrail(state, trace, now);
}
const rendered = drawLineTrails(g, trails, scope, now, lineFadeMs, settings);
const rendered = drawLineTrails(g, trails, state.trailPool, scope, now, lineFadeMs, settings);
if (!rendered && !xyReady) drawIdleMessage(g, scope);
}
} else {
if (lineFadeMs <= 0) {
trails.length = 0;
clearTrails(state);
if (hasTrace) {
drawPointTrace(g, trace, scope, settings);
} else if (!xyReady) {
@@ -520,9 +551,9 @@ function renderTrace(g, state, trace, scope, style, xyReady, settings) {
}
} else {
if (hasTrace && trace.count > 0) {
addLineTrail(trails, trace, now);
addLineTrail(state, trace, now);
}
const rendered = drawPointTrails(g, trails, scope, now, lineFadeMs, settings);
const rendered = drawPointTrails(g, trails, state.trailPool, scope, now, lineFadeMs, settings);
if (!rendered && !xyReady) {
drawIdleMessage(g, scope);
}
@@ -530,17 +561,36 @@ function renderTrace(g, state, trace, scope, style, xyReady, settings) {
}
}
function addLineTrail(trails, trace, timestamp) {
const coords = new Float32Array(trace.count * 2);
function addLineTrail(state, trace, timestamp) {
const trails = state.lineTrails || (state.lineTrails = []);
const pool = state.trailPool || (state.trailPool = []);
while (trails.length >= MAX_TRAIL_FRAMES) releaseTrail(pool, trails.shift());
let coords = pool.pop();
if (!(coords instanceof Float32Array) || coords.length < trace.count * 2) {
coords = new Float32Array(trace.count * 2);
}
coords.set(trace.buffer.subarray(0, trace.count * 2));
trails.push({ coords, count: trace.count, time: timestamp });
}
function drawLineTrails(g, trails, scope, now, fadeMs, settings) {
function releaseTrail(pool, trail) {
if (trail?.coords instanceof Float32Array && pool.length < MAX_TRAIL_FRAMES) {
pool.push(trail.coords);
}
}
function clearTrails(state) {
const trails = state.lineTrails || [];
const pool = state.trailPool || (state.trailPool = []);
for (const trail of trails) releaseTrail(pool, trail);
trails.length = 0;
}
function drawLineTrails(g, trails, pool, scope, now, fadeMs, settings) {
if (!trails.length) return false;
const cutoff = now - fadeMs;
const keep = [];
let keepCount = 0;
let drawn = false;
g.save();
@@ -549,13 +599,19 @@ function drawLineTrails(g, trails, scope, now, fadeMs, settings) {
g.clip();
for (const trail of trails) {
if (trail.time < cutoff) continue;
if (trail.time < cutoff) {
releaseTrail(pool, trail);
continue;
}
const age = now - trail.time;
const fade = Math.max(0, 1 - age / fadeMs);
if (fade <= 0) continue;
if (fade <= 0) {
releaseTrail(pool, trail);
continue;
}
drawn = true;
keep.push(trail);
trails[keepCount++] = trail;
g.save();
g.globalAlpha = (settings?.trailBaseAlpha ?? 0.35) + (settings?.trailBoostAlpha ?? 0.65) * fade;
@@ -568,8 +624,7 @@ function drawLineTrails(g, trails, scope, now, fadeMs, settings) {
g.restore();
trails.length = 0;
Array.prototype.push.apply(trails, keep);
trails.length = keepCount;
return drawn;
}
@@ -611,11 +666,11 @@ function beginTracePath(g, coords, count, settings) {
g.quadraticCurveTo(coords[prev], coords[prev + 1], coords[last], coords[last + 1]);
}
function drawPointTrails(g, trails, scope, now, fadeMs, settings) {
function drawPointTrails(g, trails, pool, scope, now, fadeMs, settings) {
if (!trails.length) return false;
const cutoff = now - fadeMs;
const keep = [];
let keepCount = 0;
let drawn = false;
const pointSize = settings?.pointSize ?? 1.4;
const half = pointSize / 2;
@@ -626,13 +681,19 @@ function drawPointTrails(g, trails, scope, now, fadeMs, settings) {
g.clip();
for (const trail of trails) {
if (trail.time < cutoff) continue;
if (trail.time < cutoff) {
releaseTrail(pool, trail);
continue;
}
const age = now - trail.time;
const fade = Math.max(0, 1 - age / fadeMs);
if (fade <= 0) continue;
if (fade <= 0) {
releaseTrail(pool, trail);
continue;
}
drawn = true;
keep.push(trail);
trails[keepCount++] = trail;
for (let i = 0; i < trail.count; i++) {
const idx = i * 2;
@@ -645,8 +706,7 @@ function drawPointTrails(g, trails, scope, now, fadeMs, settings) {
g.restore();
trails.length = 0;
Array.prototype.push.apply(trails, keep);
trails.length = keepCount;
return drawn;
}
@@ -703,6 +763,10 @@ function getNow() {
function computeAgcGain(state, xyData, nowTs) {
const attackTau = 0.001; // 1 ms
const releaseDbPerS = 10;
if (xyData.seq > 0 && xyData.seq === state.agcLastXySeq) {
return { gainDb: state.agcGainDb, gain: dbToLinear(state.agcGainDb) };
}
state.agcLastXySeq = xyData.seq;
const dt = state.agcLastTs ? Math.max(0, (nowTs - state.agcLastTs) / 1000) : 0;
state.agcLastTs = nowTs;
@@ -785,7 +849,7 @@ function drawCorrelationBar(g, centerX, y, w, h, val, negativePeak) {
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) {
if (Number.isFinite(negativePeak) && negativePeak < -0.001) {
const markerX = clampCenter(mid + (clamp1(negativePeak) * 0.5) * w);
g.fillStyle = WARN_COLOR;
g.beginPath();