From caec1edfcf2d38de2c8df87a7130c17e998703a5 Mon Sep 17 00:00:00 2001 From: Mikei386 <44135113+Mikei386@users.noreply.github.com> Date: Wed, 22 Jul 2026 07:48:05 +0200 Subject: [PATCH] Add correlation marker modes --- scripts/test_goniometer.mjs | 29 ++++++++++++++++++++++++ www/core/config.js | 5 +++++ www/index.html | 10 ++++++++- www/ui/options.js | 8 +++++++ www/views/goniometer_rtw.js | 44 +++++++++++++++++++++++++++++++++++-- 5 files changed, 93 insertions(+), 3 deletions(-) diff --git a/scripts/test_goniometer.mjs b/scripts/test_goniometer.mjs index bb824d3..4b61f3a 100644 --- a/scripts/test_goniometer.mjs +++ b/scripts/test_goniometer.mjs @@ -8,6 +8,10 @@ assert.ok(functionSource, 'persistence resolver must remain testable'); const context = vm.createContext({ Number, String, Math }); vm.runInContext(functionSource.replace('export function', 'function'), context); +const markerFunctionSource = source.match(/export function resolveCorrelationNegativeMarker[\s\S]*?\n\}/)?.[0]; +assert.ok(markerFunctionSource, 'correlation marker resolver must remain testable'); +vm.runInContext(`const CORR_HOLD_MS = 4000;\n${markerFunctionSource.replace('export function', 'function')}`, context); + assert.equal(context.resolveGoniometerPersistenceMs({ GONIO_PERSISTENCE_MODE: 'fast' }), 50); assert.equal(context.resolveGoniometerPersistenceMs({ GONIO_PERSISTENCE_MODE: 'medium' }), 150); assert.equal(context.resolveGoniometerPersistenceMs({ GONIO_PERSISTENCE_MODE: 'slow' }), 300); @@ -28,4 +32,29 @@ assert.match(source, /pool\.pop\(\)/, 'trail buffers must be reused'); assert.doesNotMatch(source, /utils\.correlation\(/, 'browser must not recalculate or smooth backend correlation'); +const markerState = { + corrNegativeMarkerMode: 'memory', + corrHoldPeak: 0, + corrHoldUntil: 0, + corrResetToken: 0, +}; +assert.equal(context.resolveCorrelationNegativeMarker( + markerState, { CORR_NEGATIVE_MARKER_MODE: 'memory' }, -0.2, -0.8, 1000 +), -0.8, 'memory mode must display the continuous DSP negative peak'); +assert.equal(context.resolveCorrelationNegativeMarker( + markerState, { CORR_NEGATIVE_MARKER_MODE: 'off' }, -0.9, -0.9, 1100 +), 0, 'off mode must hide the negative marker'); +assert.equal(context.resolveCorrelationNegativeMarker( + markerState, { CORR_NEGATIVE_MARKER_MODE: 'hold' }, -0.4, -0.9, 2000 +), -0.4, 'hold mode must start from the current correlation'); +assert.equal(context.resolveCorrelationNegativeMarker( + markerState, { CORR_NEGATIVE_MARKER_MODE: 'hold' }, -0.2, -0.9, 5000 +), -0.4, 'hold mode must retain its negative peak for four seconds'); +assert.equal(context.resolveCorrelationNegativeMarker( + markerState, { CORR_NEGATIVE_MARKER_MODE: 'hold' }, -0.2, -0.9, 6000 +), -0.2, 'hold mode must release to the current value after four seconds'); +assert.equal(context.resolveCorrelationNegativeMarker( + markerState, { CORR_NEGATIVE_MARKER_MODE: 'hold', CORR_RESET_TOKEN: 1 }, 0.2, -0.9, 6100 +), 0, 'reset must clear the four-second hold as well'); + console.log('goniometer regression tests passed'); diff --git a/www/core/config.js b/www/core/config.js index 4a5ef30..76efce9 100644 --- a/www/core/config.js +++ b/www/core/config.js @@ -190,6 +190,7 @@ const CONFIG = { CORR_ZERO_ON_SILENCE: false, CORR_SILENCE_THRESHOLD_RMS_DBFS: -75, CORR_RESPONSE_S: 1.0, + CORR_NEGATIVE_MARKER_MODE: 'memory', CORR_RESET_TOKEN: 0, XY_POINTS: 1024, XY_STYLE: 'lines', @@ -958,6 +959,10 @@ function loadConfig(opts = {}) { CONFIG.GONIO_MANUAL_GAIN_DB = Math.max(-35, Math.min(35, Math.round(fallback / 5) * 5)); } CONFIG.CORR_RESPONSE_S = Number(CONFIG.CORR_RESPONSE_S) >= 1.75 ? 2.5 : 1.0; + const corrNegativeMarkerMode = String(CONFIG.CORR_NEGATIVE_MARKER_MODE || 'memory').toLowerCase(); + CONFIG.CORR_NEGATIVE_MARKER_MODE = ['memory', 'hold', 'off'].includes(corrNegativeMarkerMode) + ? corrNegativeMarkerMode + : 'memory'; CONFIG.CORR_RESET_TOKEN = Math.max(0, Math.floor(Number(CONFIG.CORR_RESET_TOKEN) || 0)); const persistenceMode = String(CONFIG.GONIO_PERSISTENCE_MODE || 'fast').toLowerCase(); CONFIG.GONIO_PERSISTENCE_MODE = ['fast', 'medium', 'slow', 'custom'].includes(persistenceMode) diff --git a/www/index.html b/www/index.html index e0730e7..8a0830f 100644 --- a/www/index.html +++ b/www/index.html @@ -534,9 +534,17 @@ Kontinuierliche DSP-Integration; unabhängig von Bildrate und ALSA-Periode. +
+ + Memory bleibt bis zum Zurücksetzen stehen; Hold speichert die negativste Anzeige vier Sekunden. +
- Setzt den Negative-Peak-Memory auf 0 zurück; der rote Marker verschwindet. + Setzt den gespeicherten Negativmarker zurück. Im Modus Off bleibt der Marker ausgeblendet.
diff --git a/www/ui/options.js b/www/ui/options.js index ee5f119..cbdb140 100644 --- a/www/ui/options.js +++ b/www/ui/options.js @@ -271,6 +271,7 @@ function syncUI() { ['opt_rmsColWarn', CONFIG.RMS_COLOR_WARN], ['opt_corrResponse', String(Number(CONFIG.CORR_RESPONSE_S) >= 1.75 ? 2.5 : 1)], + ['opt_corrNegativeMarkerMode', CONFIG.CORR_NEGATIVE_MARKER_MODE || 'memory'], ['opt_xyPoints', String(CONFIG.XY_POINTS)], ['opt_xyStyle', CONFIG.XY_STYLE], ['opt_xySilenceGate', CONFIG.XY_SILENCE_GATE_ENABLED, null, null, 'checkbox'], @@ -929,6 +930,13 @@ function wireHandlers(env) { try { env?.notifyRtaConfig?.(); } catch (_) {} return String(CONFIG.CORR_RESPONSE_S); }); + h('opt_corrNegativeMarkerMode', v => { + const mode = String(v || '').toLowerCase(); + CONFIG.CORR_NEGATIVE_MARKER_MODE = ['memory', 'hold', 'off'].includes(mode) + ? mode + : 'memory'; + return CONFIG.CORR_NEGATIVE_MARKER_MODE; + }); const corrResetPeak = E('opt_corrResetPeak'); if (corrResetPeak) corrResetPeak.onclick = () => { CONFIG.CORR_RESET_TOKEN = Math.max(0, Math.floor(Number(CONFIG.CORR_RESET_TOKEN) || 0)) + 1; diff --git a/www/views/goniometer_rtw.js b/www/views/goniometer_rtw.js index c38ffea..3cbfcdc 100644 --- a/www/views/goniometer_rtw.js +++ b/www/views/goniometer_rtw.js @@ -24,6 +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_HOLD_MS = 4000; const MAX_TRAIL_FRAMES = 48; function computePanelSlotWidth(canvasWidth, slotCount = METER_SLOTS, slotGap = 12) { @@ -48,6 +49,10 @@ export function init() { staticLayerCanvas: null, staticLayerCtx: null, staticLayerKey: '', + corrNegativeMarkerMode: 'memory', + corrHoldPeak: 0, + corrHoldUntil: 0, + corrResetToken: 0, }; } @@ -99,9 +104,16 @@ export async function render(env, state) { // 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) + const corrMemoryPeak = Number.isFinite(audio?.correlationNegativePeak) ? clamp1(audio.correlationNegativePeak) : 0; + const corrNegativeMarker = resolveCorrelationNegativeMarker( + state, + CONFIG, + corrVisual, + corrMemoryPeak, + frameNow, + ); drawCorrelationBar( g, layout.plot.x + layout.plot.w / 2, @@ -109,7 +121,7 @@ export async function render(env, state) { Math.round(Math.min(layout.scope.w * 0.75, layout.plot.w - 50)), 18, corrVisual, - corrNegativePeak, + corrNegativeMarker, ); if (slots.length) { @@ -123,6 +135,34 @@ export async function render(env, state) { } } +export function resolveCorrelationNegativeMarker(state, CONFIG = {}, current = 0, memoryPeak = 0, nowMs = 0) { + const requestedMode = String(CONFIG.CORR_NEGATIVE_MARKER_MODE || 'memory').toLowerCase(); + const mode = ['memory', 'hold', 'off'].includes(requestedMode) ? requestedMode : 'memory'; + const resetToken = Math.max(0, Math.floor(Number(CONFIG.CORR_RESET_TOKEN) || 0)); + + if (state.corrResetToken !== resetToken || state.corrNegativeMarkerMode !== mode) { + state.corrResetToken = resetToken; + state.corrNegativeMarkerMode = mode; + state.corrHoldPeak = 0; + state.corrHoldUntil = 0; + } + + if (mode === 'off') return 0; + if (mode === 'memory') return Math.max(-1, Math.min(1, Number(memoryPeak) || 0)); + + const now = Number.isFinite(nowMs) ? nowMs : 0; + const currentNegative = Math.min(0, Math.max(-1, Math.min(1, Number(current) || 0))); + const holdActive = Number.isFinite(state.corrHoldUntil) && now < state.corrHoldUntil; + if (!holdActive) { + state.corrHoldPeak = currentNegative; + state.corrHoldUntil = currentNegative < -0.001 ? now + CORR_HOLD_MS : 0; + } else if (currentNegative < state.corrHoldPeak) { + state.corrHoldPeak = currentNegative; + state.corrHoldUntil = now + CORR_HOLD_MS; + } + return Number.isFinite(state.corrHoldPeak) ? state.corrHoldPeak : 0; +} + function drawStaticLayer(g, state, rect, layout, CONFIG, slotCount) { const layer = ensureStaticLayer(state, rect, layout, CONFIG, slotCount); if (!layer) {