Improve phase trail and correlation controls

This commit is contained in:
Mikei386
2026-07-27 18:13:07 +02:00
parent 00fb59f643
commit a4fa0ceb47
13 changed files with 268 additions and 41 deletions
+1 -1
View File
@@ -17,7 +17,7 @@
<li><strong>Hardwareparameter:</strong> Tatsächlich von ALSA ausgehandelte Samplerate, Perioden- und Puffergröße werden nun durch DSP, Aufnahme, Status und Browser verwendet; die feste 48-kHz-Annahme wurde entfernt.</li>
<li><strong>Goniometer:</strong> Nur neue XY-Samplepaare werden mit einer periodengrößenunabhängigen mittleren Rate von 60 Hz übertragen. Punktbegrenzung wirkt bereits auf den Transport; begrenzte und wiederverwendbare Spurpuffer reduzieren Speicher- und Zeichenlast.</li>
<li><strong>Goniometer-Persistenz:</strong> Reproduzierbare Fast-, Medium- und Slow-Profile sowie ein freies Phoenix-Fade ergänzt. Künstliche Bézier-Verformung der Messspur entfernt; AGC und Silence-Gate arbeiten zeit- beziehungsweise fensterbasiert.</li>
<li><strong>Korrelation:</strong> Kontinuierliche DSP-Messung aus L², R² und L·R mit wählbaren 1,0/2,5 Sekunden ergänzt. Bildratenabhängige Doppelglättung entfernt; Negative-Peak-Memory, Marker und manueller Reset hinzugefügt.</li>
<li><strong>Korrelation:</strong> Kontinuierliche DSP-Messung aus L², R² und L·R mit wählbaren 0,5/1,0/2,5 Sekunden und konfigurierbarem Mono-RMS-Silence-Threshold ergänzt. Bildratenabhängige Doppelglättung entfernt; Negative-Peak-Memory, Marker und manueller Reset hinzugefügt.</li>
<li><strong>Phasenrad:</strong> Bandpass, Hilbert-Transformation und energiegewichtete L/R-Phasenmittelung laufen nun samplekontinuierlich im Audiokern statt auf ausgedünnten Browser-XY-Punkten. Paketgrenzen beeinflussen den Winkel nicht mehr; Glättung und AGC arbeiten zeitbasiert, und die doppelte AGC-Verstärkung wurde entfernt.</li>
<li><strong>True Peak, RMS und VU:</strong> True Peak verwendet den vierphasigen Referenz-FIR aus ITU-R BS.1770, besteht die EBU-Testfälle 15 bis 19 und zeigt Übersteuerungen in allen Ansichten bis +6 dBTP an. True RMS integriert samplekontinuierlich im Leistungsbereich mit Fast (125 ms), Slow (1 s) oder einem gleitenden 300-ms-Fenster; Browser-Doppelglättung und der RMS-fremde Impulse-Modus wurden entfernt. VU verwendet ein Moving-Coil-Modell mit 300-ms-Sprungantwort und 1 bis 1,5 % Überschwingen; der fälschliche Standardoffset von +1,92 dB wurde auf 0 dB korrigiert.</li>
<li><strong>Qualitätssicherung:</strong> Automatische Regressionstests für PPM, RTA, A/C/Z, mehrere Sampleraten, Korrelationssignale, Goniometertaktung, Binärprotokolle sowie Spektrogramm-Zeitbasis und Langlauf ergänzt.</li>
+10 -2
View File
@@ -1,7 +1,12 @@
// core/audio.js — Phoenix Audio-Init, Metrics-WS, Waveform/Spectrogram buffers
// Wird von main.js mit `initAudio(env)` gestartet. Nutzt env.config (CONFIG) und env.meters.
import { applyPhoenixGlobalConfig, buildPhoenixGlobalConfigPayload, saveConfig } from './config.js';
import {
applyPhoenixGlobalConfig,
buildPhoenixGlobalConfigPayload,
normalizeCorrelationResponseSeconds,
saveConfig,
} from './config.js';
import { decodePhoenixSpectroBuffer, decodePhoenixVisualsBuffer } from './binary_protocol.js';
import { getRtwCenters } from './rtw_centers.js';
@@ -943,7 +948,10 @@ export function buildRtaRuntimeConfig(CONFIG = {}) {
ppmEbuDecayDbPerS: 24 / 2.8,
lufsIWindowMin: Number.isFinite(CONFIG.LUFS_I_WINDOW_MIN) ? CONFIG.LUFS_I_WINDOW_MIN : 4,
lufsINormEnabled: !!CONFIG.LUFS_I_NORM_ENABLED,
correlationResponseS: Number(CONFIG.CORR_RESPONSE_S) >= 1.75 ? 2.5 : 1.0,
correlationResponseS: normalizeCorrelationResponseSeconds(CONFIG.CORR_RESPONSE_S),
correlationSilenceThresholdRmsDbfs: Number.isFinite(CONFIG.CORR_SILENCE_THRESHOLD_RMS_DBFS)
? Math.max(-90, Math.min(-40, CONFIG.CORR_SILENCE_THRESHOLD_RMS_DBFS))
: -75,
correlationResetToken: Math.max(0, Math.floor(Number(CONFIG.CORR_RESET_TOKEN) || 0)),
xyPoints: [128, 256, 512, 1024, 2048].includes(Number(CONFIG.XY_POINTS))
? Number(CONFIG.XY_POINTS)
+9 -1
View File
@@ -1,5 +1,13 @@
// core/config.js — zentrale CONFIG + Presets + Persistenz
export function normalizeCorrelationResponseSeconds(value) {
const seconds = Number(value);
if (!Number.isFinite(seconds) || seconds <= 0) return 1.0;
if (seconds < 0.75) return 0.5;
if (seconds < 1.75) return 1.0;
return 2.5;
}
function isLoopbackHost(host) {
const h = String(host || '').trim().toLowerCase();
return h === 'localhost' || h === '127.0.0.1' || h === '::1' || h === '[::1]';
@@ -971,7 +979,7 @@ function loadConfig(opts = {}) {
const fallback = Number.isFinite(CONFIG.GONIO_DISPLAY_GAIN_DB) ? CONFIG.GONIO_DISPLAY_GAIN_DB : 0;
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 = normalizeCorrelationResponseSeconds(CONFIG.CORR_RESPONSE_S);
const corrNegativeMarkerMode = String(CONFIG.CORR_NEGATIVE_MARKER_MODE || 'memory').toLowerCase();
CONFIG.CORR_NEGATIVE_MARKER_MODE = ['memory', 'hold', 'off'].includes(corrNegativeMarkerMode)
? corrNegativeMarkerMode
+2 -1
View File
@@ -521,7 +521,7 @@
<input id="opt_xySilenceGate" type="checkbox" checked>
Silence-Gate aktivieren (RMS-basiert)
</label>
<small>Blendet nur die Goniometer-Spur bei Stille aus; die Korrelation wird unabhängig kontinuierlich gemessen.</small>
<small>Blendet die Goniometer-Spur bei Stille aus; der Threshold setzt außerdem die Korrelation auf Neutralstellung.</small>
</div>
<div class="opt">
<label>Silence-Threshold [dBFS RMS]</label>
@@ -530,6 +530,7 @@
</div>
<div class="opt"><label>Korrelation Ansprechzeit</label>
<select id="opt_corrResponse">
<option value="0.5">0,5 s</option>
<option value="1">1,0 s</option>
<option value="2.5">2,5 s</option>
</select>
+5 -4
View File
@@ -1,7 +1,7 @@
// ui/options.js — bindet das Options-Panel an CONFIG (lesen/schreiben), Presets, Export/Import
// Erwartet vorhandenes DOM aus index.html. Nutzt localStorage-Key 'analyzer_config'.
import { CONFIG, saveConfig, loadConfig, applyAlignmentProfile, applyRtaBpoSelection, applySoftwarePreset, loadSoftwarePreset, saveSoftwarePreset, loadLayoutPreset, saveLayoutPreset, exportSoftwarePreset, importSoftwarePreset, updateVuReference, resetConfigToDefaults, updateInputOffsetGain } from '../core/config.js';
import { CONFIG, saveConfig, loadConfig, applyAlignmentProfile, applyRtaBpoSelection, applySoftwarePreset, loadSoftwarePreset, saveSoftwarePreset, loadLayoutPreset, saveLayoutPreset, exportSoftwarePreset, importSoftwarePreset, updateVuReference, resetConfigToDefaults, updateInputOffsetGain, normalizeCorrelationResponseSeconds } from '../core/config.js';
import { clamp, clampPow2 } from '../core/utils.js';
import { bindOptionsViewportAssist } from './options_viewport.js';
import { setupLazyChangelog } from './changelog.js';
@@ -184,7 +184,7 @@ function syncUI() {
['opt_rmsColNorm', CONFIG.RMS_COLOR_NORMAL],
['opt_rmsColWarn', CONFIG.RMS_COLOR_WARN],
['opt_corrResponse', String(Number(CONFIG.CORR_RESPONSE_S) >= 1.75 ? 2.5 : 1)],
['opt_corrResponse', String(normalizeCorrelationResponseSeconds(CONFIG.CORR_RESPONSE_S))],
['opt_corrNegativeMarkerMode', CONFIG.CORR_NEGATIVE_MARKER_MODE || 'memory'],
['opt_xyPoints', String(CONFIG.XY_POINTS)],
['opt_xyStyle', CONFIG.XY_STYLE],
@@ -457,7 +457,8 @@ function wireHandlers(env) {
if (!el) return;
const notifyRta = /^opt_rta/i.test(id)
|| id === 'opt_lufsIWindowMin'
|| id === 'opt_lufsINorm';
|| id === 'opt_lufsINorm'
|| id === 'opt_xySilenceThr';
const inputType = String(el.type || '').toLowerCase();
const commitOnInput = isCheckbox || inputType === 'range' || inputType === 'color';
@@ -813,7 +814,7 @@ function wireHandlers(env) {
});
h('opt_corrResponse', v => {
CONFIG.CORR_RESPONSE_S = Number(v) >= 1.75 ? 2.5 : 1.0;
CONFIG.CORR_RESPONSE_S = normalizeCorrelationResponseSeconds(v);
try { env?.notifyRtaConfig?.(); } catch (_) {}
return String(CONFIG.CORR_RESPONSE_S);
});
+50 -12
View File
@@ -29,6 +29,8 @@ const PHASE_RADIUS_TAU_S = 0.085;
const PHASE_TRAIL_FADE_MS = 2000;
const PHASE_TRAIL_MIN_STEP_MS = 1000 / 45;
const PHASE_TRAIL_MIN_DIST_PX = 1.5;
const PHASE_TRAIL_MAX_ANGLE_STEP_RAD = (2 * Math.PI) / 180;
const PHASE_TRAIL_MAX_POINTS = 1024;
const PHASE_SECTORS = [
{ startDeg: -30, endDeg: 30, color: 'rgba(72,210,150,0.3)' },
{ startDeg: 30, endDeg: 60, color: 'rgba(255,214,120,0.25)' },
@@ -517,31 +519,64 @@ function updatePhaseTrail(state, wheel, CONFIG, now) {
}
const radiusNorm = Math.max(0, Number(state?.smoothRadius) || 0);
if (radiusNorm > 0.002 && Number.isFinite(state?.smoothPhase)) {
const length = radiusNorm * wheel.radius;
const x = wheel.cx + Math.cos(state.smoothPhase) * length;
const y = wheel.cy + Math.sin(state.smoothPhase) * length;
const color = trailColorForAngle(state.smoothPhase);
const phase = wrapAngle(state.smoothPhase);
const point = createPhaseTrailPoint(wheel, phase, radiusNorm, now);
const last = state.phaseTrail.length ? state.phaseTrail[state.phaseTrail.length - 1] : null;
if (!last) {
state.phaseTrail.push({ x, y, t: now, color });
state.phaseTrail.push(point);
} else {
const dt = now - last.t;
const dx = x - last.x;
const dy = y - last.y;
const dx = point.x - last.x;
const dy = point.y - last.y;
const dist2 = dx * dx + dy * dy;
if (dt >= PHASE_TRAIL_MIN_STEP_MS || dist2 >= PHASE_TRAIL_MIN_DIST_PX * PHASE_TRAIL_MIN_DIST_PX || last.color !== color) {
state.phaseTrail.push({ x, y, t: now, color });
if (dt >= PHASE_TRAIL_MIN_STEP_MS || dist2 >= PHASE_TRAIL_MIN_DIST_PX * PHASE_TRAIL_MIN_DIST_PX || last.color !== point.color) {
appendPhaseTrailSegment(state.phaseTrail, wheel, last, phase, radiusNorm, now);
} else {
last.x = x;
last.y = y;
last.x = point.x;
last.y = point.y;
last.t = now;
last.color = color;
last.color = point.color;
last.phase = phase;
last.radiusNorm = radiusNorm;
}
}
}
trimPhaseTrail(state, CONFIG, now);
}
function createPhaseTrailPoint(wheel, phase, radiusNorm, timestamp) {
const length = radiusNorm * wheel.radius;
return {
x: wheel.cx + Math.cos(phase) * length,
y: wheel.cy + Math.sin(phase) * length,
t: timestamp,
color: trailColorForAngle(phase),
phase,
radiusNorm,
};
}
function appendPhaseTrailSegment(trail, wheel, last, phase, radiusNorm, timestamp) {
const startPhase = Number.isFinite(last?.phase) ? last.phase : phase;
const startRadius = Number.isFinite(last?.radiusNorm) ? last.radiusNorm : radiusNorm;
const phaseDelta = wrapAngle(phase - startPhase);
const steps = Math.max(1, Math.ceil(Math.abs(phaseDelta) / PHASE_TRAIL_MAX_ANGLE_STEP_RAD));
const startTime = Number.isFinite(last?.t) ? last.t : timestamp;
for (let step = 1; step <= steps; step++) {
const fraction = step / steps;
const interpolatedPhase = wrapAngle(startPhase + phaseDelta * fraction);
const interpolatedRadius = lerp(startRadius, radiusNorm, fraction);
const interpolatedTime = lerp(startTime, timestamp, fraction);
trail.push(createPhaseTrailPoint(
wheel,
interpolatedPhase,
interpolatedRadius,
interpolatedTime,
));
}
}
function trimPhaseTrail(state, CONFIG, now) {
if (!state.phaseTrail) state.phaseTrail = [];
if (!CONFIG?.PHASE_TRAIL_ENABLED) {
@@ -557,6 +592,9 @@ function trimPhaseTrail(state, CONFIG, now) {
}
}
state.phaseTrail.length = write;
if (state.phaseTrail.length > PHASE_TRAIL_MAX_POINTS) {
state.phaseTrail.splice(0, state.phaseTrail.length - PHASE_TRAIL_MAX_POINTS);
}
}
function drawPhaseTrail(g, wheel, state, CONFIG, now) {