Add correlation marker modes
This commit is contained in:
@@ -8,6 +8,10 @@ assert.ok(functionSource, 'persistence resolver must remain testable');
|
|||||||
const context = vm.createContext({ Number, String, Math });
|
const context = vm.createContext({ Number, String, Math });
|
||||||
vm.runInContext(functionSource.replace('export function', 'function'), context);
|
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: 'fast' }), 50);
|
||||||
assert.equal(context.resolveGoniometerPersistenceMs({ GONIO_PERSISTENCE_MODE: 'medium' }), 150);
|
assert.equal(context.resolveGoniometerPersistenceMs({ GONIO_PERSISTENCE_MODE: 'medium' }), 150);
|
||||||
assert.equal(context.resolveGoniometerPersistenceMs({ GONIO_PERSISTENCE_MODE: 'slow' }), 300);
|
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\(/,
|
assert.doesNotMatch(source, /utils\.correlation\(/,
|
||||||
'browser must not recalculate or smooth backend 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');
|
console.log('goniometer regression tests passed');
|
||||||
|
|||||||
@@ -190,6 +190,7 @@ const CONFIG = {
|
|||||||
CORR_ZERO_ON_SILENCE: false,
|
CORR_ZERO_ON_SILENCE: false,
|
||||||
CORR_SILENCE_THRESHOLD_RMS_DBFS: -75,
|
CORR_SILENCE_THRESHOLD_RMS_DBFS: -75,
|
||||||
CORR_RESPONSE_S: 1.0,
|
CORR_RESPONSE_S: 1.0,
|
||||||
|
CORR_NEGATIVE_MARKER_MODE: 'memory',
|
||||||
CORR_RESET_TOKEN: 0,
|
CORR_RESET_TOKEN: 0,
|
||||||
XY_POINTS: 1024,
|
XY_POINTS: 1024,
|
||||||
XY_STYLE: 'lines',
|
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.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;
|
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));
|
CONFIG.CORR_RESET_TOKEN = Math.max(0, Math.floor(Number(CONFIG.CORR_RESET_TOKEN) || 0));
|
||||||
const persistenceMode = String(CONFIG.GONIO_PERSISTENCE_MODE || 'fast').toLowerCase();
|
const persistenceMode = String(CONFIG.GONIO_PERSISTENCE_MODE || 'fast').toLowerCase();
|
||||||
CONFIG.GONIO_PERSISTENCE_MODE = ['fast', 'medium', 'slow', 'custom'].includes(persistenceMode)
|
CONFIG.GONIO_PERSISTENCE_MODE = ['fast', 'medium', 'slow', 'custom'].includes(persistenceMode)
|
||||||
|
|||||||
+9
-1
@@ -534,9 +534,17 @@
|
|||||||
</select>
|
</select>
|
||||||
<small>Kontinuierliche DSP-Integration; unabhängig von Bildrate und ALSA-Periode.</small>
|
<small>Kontinuierliche DSP-Integration; unabhängig von Bildrate und ALSA-Periode.</small>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="opt"><label>Korrelations-Negativmarker</label>
|
||||||
|
<select id="opt_corrNegativeMarkerMode">
|
||||||
|
<option value="memory">Memory</option>
|
||||||
|
<option value="hold">Hold (4 Sekunden)</option>
|
||||||
|
<option value="off">Off</option>
|
||||||
|
</select>
|
||||||
|
<small>Memory bleibt bis zum Zurücksetzen stehen; Hold speichert die negativste Anzeige vier Sekunden.</small>
|
||||||
|
</div>
|
||||||
<div class="opt">
|
<div class="opt">
|
||||||
<button id="opt_corrResetPeak" type="button">Negativspitze zurücksetzen</button>
|
<button id="opt_corrResetPeak" type="button">Negativspitze zurücksetzen</button>
|
||||||
<small>Setzt den Negative-Peak-Memory auf 0 zurück; der rote Marker verschwindet.</small>
|
<small>Setzt den gespeicherten Negativmarker zurück. Im Modus Off bleibt der Marker ausgeblendet.</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="opt"><label>Goniometer Display Gain (dB)</label>
|
<div class="opt"><label>Goniometer Display Gain (dB)</label>
|
||||||
<input id="opt_goniGain" type="range" min="-35" max="35" step="5"><span id="val_goniGain"></span>
|
<input id="opt_goniGain" type="range" min="-35" max="35" step="5"><span id="val_goniGain"></span>
|
||||||
|
|||||||
@@ -271,6 +271,7 @@ function syncUI() {
|
|||||||
['opt_rmsColWarn', CONFIG.RMS_COLOR_WARN],
|
['opt_rmsColWarn', CONFIG.RMS_COLOR_WARN],
|
||||||
|
|
||||||
['opt_corrResponse', String(Number(CONFIG.CORR_RESPONSE_S) >= 1.75 ? 2.5 : 1)],
|
['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_xyPoints', String(CONFIG.XY_POINTS)],
|
||||||
['opt_xyStyle', CONFIG.XY_STYLE],
|
['opt_xyStyle', CONFIG.XY_STYLE],
|
||||||
['opt_xySilenceGate', CONFIG.XY_SILENCE_GATE_ENABLED, null, null, 'checkbox'],
|
['opt_xySilenceGate', CONFIG.XY_SILENCE_GATE_ENABLED, null, null, 'checkbox'],
|
||||||
@@ -929,6 +930,13 @@ function wireHandlers(env) {
|
|||||||
try { env?.notifyRtaConfig?.(); } catch (_) {}
|
try { env?.notifyRtaConfig?.(); } catch (_) {}
|
||||||
return String(CONFIG.CORR_RESPONSE_S);
|
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');
|
const corrResetPeak = E('opt_corrResetPeak');
|
||||||
if (corrResetPeak) corrResetPeak.onclick = () => {
|
if (corrResetPeak) corrResetPeak.onclick = () => {
|
||||||
CONFIG.CORR_RESET_TOKEN = Math.max(0, Math.floor(Number(CONFIG.CORR_RESET_TOKEN) || 0)) + 1;
|
CONFIG.CORR_RESET_TOKEN = Math.max(0, Math.floor(Number(CONFIG.CORR_RESET_TOKEN) || 0)) + 1;
|
||||||
|
|||||||
@@ -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 BASE_GONIO_SCALE = (BASE_TARGET / ALIGN_PEAK) * Math.pow(10, -BASE_HEADROOM_DB / 20) * BASE_ZOOM;
|
||||||
const AGC_TARGET_DB = linearToDb(ALIGN_PEAK);
|
const AGC_TARGET_DB = linearToDb(ALIGN_PEAK);
|
||||||
const CORR_SILENCE_THRESHOLD_DEFAULT = -75;
|
const CORR_SILENCE_THRESHOLD_DEFAULT = -75;
|
||||||
|
const CORR_HOLD_MS = 4000;
|
||||||
const MAX_TRAIL_FRAMES = 48;
|
const MAX_TRAIL_FRAMES = 48;
|
||||||
|
|
||||||
function computePanelSlotWidth(canvasWidth, slotCount = METER_SLOTS, slotGap = 12) {
|
function computePanelSlotWidth(canvasWidth, slotCount = METER_SLOTS, slotGap = 12) {
|
||||||
@@ -48,6 +49,10 @@ export function init() {
|
|||||||
staticLayerCanvas: null,
|
staticLayerCanvas: null,
|
||||||
staticLayerCtx: null,
|
staticLayerCtx: null,
|
||||||
staticLayerKey: '',
|
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
|
// Correlation is measured continuously in the audio DSP. Do not add a
|
||||||
// second, frame-rate-dependent browser integration here.
|
// second, frame-rate-dependent browser integration here.
|
||||||
const corrVisual = Number.isFinite(audio?.correlation) ? clamp1(audio.correlation) : 0;
|
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)
|
? clamp1(audio.correlationNegativePeak)
|
||||||
: 0;
|
: 0;
|
||||||
|
const corrNegativeMarker = resolveCorrelationNegativeMarker(
|
||||||
|
state,
|
||||||
|
CONFIG,
|
||||||
|
corrVisual,
|
||||||
|
corrMemoryPeak,
|
||||||
|
frameNow,
|
||||||
|
);
|
||||||
drawCorrelationBar(
|
drawCorrelationBar(
|
||||||
g,
|
g,
|
||||||
layout.plot.x + layout.plot.w / 2,
|
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)),
|
Math.round(Math.min(layout.scope.w * 0.75, layout.plot.w - 50)),
|
||||||
18,
|
18,
|
||||||
corrVisual,
|
corrVisual,
|
||||||
corrNegativePeak,
|
corrNegativeMarker,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (slots.length) {
|
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) {
|
function drawStaticLayer(g, state, rect, layout, CONFIG, slotCount) {
|
||||||
const layer = ensureStaticLayer(state, rect, layout, CONFIG, slotCount);
|
const layer = ensureStaticLayer(state, rect, layout, CONFIG, slotCount);
|
||||||
if (!layer) {
|
if (!layer) {
|
||||||
|
|||||||
Reference in New Issue
Block a user