Correct audio metering and realtime displays
This commit is contained in:
+13
-1
@@ -444,6 +444,11 @@ async function applyIncomingAudioPacket(env, packet, CONFIG, sampleTs = performa
|
||||
if (typeof d.correlationNegativePeak === 'number') {
|
||||
env.audio.correlationNegativePeak = d.correlationNegativePeak;
|
||||
}
|
||||
env.audio.phaseAngleRad = typeof d.phaseAngleRad === 'number' ? d.phaseAngleRad : null;
|
||||
env.audio.phaseSeq = Number.isFinite(seq) ? seq : ((env.audio.phaseSeq || 0) + 1);
|
||||
if (typeof d.phaseCoherence === 'number') env.audio.phaseCoherence = d.phaseCoherence;
|
||||
if (typeof d.phaseLevel === 'number') env.audio.phaseLevel = d.phaseLevel;
|
||||
if (typeof d.phasePeak === 'number') env.audio.phasePeak = d.phasePeak;
|
||||
if (d.waveL && env.audio.pushWaveSamples) {
|
||||
const channelCount = d.waveChannels || (d.waveR ? 2 : 1);
|
||||
env.audio.sampleRate = d.sampleRate || env.audio.sampleRate || 48000;
|
||||
@@ -502,6 +507,10 @@ function buildPhoenixMeterPacket(frame) {
|
||||
const ppmBoxR = Number(frame?.ppm_box_r);
|
||||
const correlation = Number(frame?.correlation);
|
||||
const correlationNegativePeak = Number(frame?.correlation_negative_peak);
|
||||
const phaseAngleRad = Number(frame?.phase_angle_rad);
|
||||
const phaseCoherence = Number(frame?.phase_coherence);
|
||||
const phaseLevel = Number(frame?.phase_level);
|
||||
const phasePeak = Number(frame?.phase_peak);
|
||||
const xyL = Array.isArray(frame?.xy_l) ? frame.xy_l : null;
|
||||
const xyR = Array.isArray(frame?.xy_r) ? frame.xy_r : null;
|
||||
const waveL = Array.isArray(frame?.wave_l) && frame.wave_l.length ? frame.wave_l : null;
|
||||
@@ -515,6 +524,10 @@ function buildPhoenixMeterPacket(frame) {
|
||||
correlationNegativePeak: Number.isFinite(correlationNegativePeak)
|
||||
? Math.max(-1, Math.min(1, correlationNegativePeak))
|
||||
: 0,
|
||||
phaseAngleRad: Number.isFinite(phaseAngleRad) ? phaseAngleRad : null,
|
||||
phaseCoherence: Number.isFinite(phaseCoherence) ? Math.max(0, Math.min(1, phaseCoherence)) : 0,
|
||||
phaseLevel: Number.isFinite(phaseLevel) ? Math.max(0, phaseLevel) : 0,
|
||||
phasePeak: Number.isFinite(phasePeak) ? Math.max(0, phasePeak) : 0,
|
||||
rmsL: Number.isFinite(rmsL) ? rmsL : -120,
|
||||
rmsR: Number.isFinite(rmsR) ? rmsR : -120,
|
||||
tpL: Number.isFinite(tpL) ? tpL : -120,
|
||||
@@ -926,7 +939,6 @@ export function buildRtaRuntimeConfig(CONFIG = {}) {
|
||||
inputOffsetDbR: Number.isFinite(CONFIG.INPUT_OFFSET_DB_R) ? CONFIG.INPUT_OFFSET_DB_R : -5,
|
||||
ppmDinAttackMs: 10,
|
||||
ppmDinDecayDbPerS: 20 / 1.5,
|
||||
ppmDinFastAttack: !!CONFIG.PPM_DIN_FAST_ATTACK,
|
||||
ppmEbuAttackMs: 10,
|
||||
ppmEbuDecayDbPerS: 24 / 2.8,
|
||||
lufsIWindowMin: Number.isFinite(CONFIG.LUFS_I_WINDOW_MIN) ? CONFIG.LUFS_I_WINDOW_MIN : 4,
|
||||
|
||||
+28
-20
@@ -106,7 +106,7 @@ const CONFIG = {
|
||||
VU_REF_IS_PLUS4: true,
|
||||
// 0 VU soll +4 dBu entsprechen → DIGITAL_REF + 4 dB.
|
||||
VU_DBFS_REF: -14.0,
|
||||
VU_OFFSET_DB: 1.92,
|
||||
VU_OFFSET_DB: 0.0,
|
||||
VU_HEADER_SHOW_VALUE: false,
|
||||
TP_OFFSET_DB: 0.0,
|
||||
TP_HEADER_SHOW_VALUE: false,
|
||||
@@ -119,8 +119,9 @@ const CONFIG = {
|
||||
PPM_RED_BAR_ONLY: true,
|
||||
TP_RED_BAR_ONLY: true,
|
||||
RMS_RED_BAR_ONLY: true,
|
||||
RMS_TC_MODE: 'fast', // 'impulse' (35 ms), 'fast' (125 ms), 'slow' (1000 ms), 'none'
|
||||
RMS_TC_MS: null, // optional override in ms (falls gesetzt, überschreibt den Modus)
|
||||
// True-RMS integration is performed sample-continuously on linear power in
|
||||
// the backend. "window" is a rectangular sliding 300-ms measurement.
|
||||
RMS_TC_MODE: 'fast', // 'fast', 'slow', 'window'
|
||||
VU_COLOR_NORMAL: '#ffe066',
|
||||
VU_COLOR_WARN: '#ff3b3b',
|
||||
PPM_DIN_COLOR_NORMAL: '#ffe066',
|
||||
@@ -161,9 +162,6 @@ const CONFIG = {
|
||||
PPM_DIN_RED_START: 0,
|
||||
// Fixed RTW/DIN profile: 10 ms integration and 20 dB return in 1.5 s.
|
||||
PPM_DIN_ATTACK_MS: 10,
|
||||
// Non-normative: when enabled, PPM DIN attack becomes instant (no integration).
|
||||
// Useful to compensate perceived meter lag caused by device / pipeline latency.
|
||||
PPM_DIN_FAST_ATTACK: false,
|
||||
PPM_DIN_DECAY_DB_PER_S: 20 / 1.5,
|
||||
// Hold time for DIN PPM: verlängert auf 1000 ms (1 s), damit der Peak‑Hold
|
||||
// auch bei kurzen Transienten deutlich sichtbar bleibt. Gemäß
|
||||
@@ -589,9 +587,9 @@ const PHOENIX_GLOBAL_OPTION_KEYS = Object.freeze([
|
||||
'LR_FRACTIONAL_DELAY_SAMPLES',
|
||||
'PPM_DIN_ATTACK_MS',
|
||||
'PPM_DIN_DECAY_DB_PER_S',
|
||||
'PPM_DIN_FAST_ATTACK',
|
||||
'PPM_EBU_ATTACK_MS',
|
||||
'PPM_EBU_DECAY_DB_PER_S',
|
||||
'RMS_TC_MODE',
|
||||
'LUFS_I_WINDOW_MIN',
|
||||
'LUFS_I_NORM_ENABLED',
|
||||
'PPM_DIN_LOUDNESS_BOXES',
|
||||
@@ -679,8 +677,8 @@ const BROADCAST_STANDARDS = {
|
||||
};
|
||||
|
||||
const STORAGE_KEY = 'analyzer_config';
|
||||
const VU_OFFSET_ALIGNMENT_DB = 1.92;
|
||||
const VU_OFFSET_ALIGNMENT_MIGRATION_KEY = 'migrate_vu_offset_alignment_plus2_v1';
|
||||
const VU_OFFSET_DEFAULT_DB = 0.0;
|
||||
const VU_OFFSET_ZERO_MIGRATION_KEY = 'migrate_vu_offset_default_zero_v2';
|
||||
const INPUT_OFFSET_DB_MIN = -60;
|
||||
const INPUT_OFFSET_DB_MAX = 20;
|
||||
const LR_DELAY_ZERO_MIGRATION_KEY = 'migrate_lr_delay_zero_to_0_05_v1';
|
||||
@@ -845,21 +843,23 @@ function loadConfig(opts = {}) {
|
||||
if (!Number.isFinite(CONFIG.PPM_DIN_LOUDNESS_OFFSET_DB)) CONFIG.PPM_DIN_LOUDNESS_OFFSET_DB = 0;
|
||||
CONFIG.PPM_DIN_LOUDNESS_OFFSET_DB = Math.max(-7, Math.min(7, CONFIG.PPM_DIN_LOUDNESS_OFFSET_DB));
|
||||
try {
|
||||
const needsVuMigration = !localStorage.getItem(VU_OFFSET_ALIGNMENT_MIGRATION_KEY);
|
||||
const needsVuMigration = !localStorage.getItem(VU_OFFSET_ZERO_MIGRATION_KEY);
|
||||
const savedVuOffset = Number(saved?.VU_OFFSET_DB);
|
||||
const oldDefaultOffset = Number(CONFIG_DEFAULTS?.VU_OFFSET_DB);
|
||||
const looksLikeOldDefault = !Number.isFinite(savedVuOffset)
|
||||
|| Math.abs(savedVuOffset - oldDefaultOffset) < 0.02
|
||||
const looksLikeOldAutomaticOffset = !Number.isFinite(savedVuOffset)
|
||||
|| Math.abs(savedVuOffset - 1.92) < 0.02
|
||||
|| Math.abs(savedVuOffset - (-0.08)) < 0.02
|
||||
|| Math.abs(savedVuOffset - (-0.05)) < 0.02;
|
||||
if (needsVuMigration && looksLikeOldDefault) {
|
||||
CONFIG.VU_OFFSET_DB = VU_OFFSET_ALIGNMENT_DB;
|
||||
if (needsVuMigration && looksLikeOldAutomaticOffset) {
|
||||
CONFIG.VU_OFFSET_DB = VU_OFFSET_DEFAULT_DB;
|
||||
}
|
||||
if (needsVuMigration) localStorage.setItem(VU_OFFSET_ALIGNMENT_MIGRATION_KEY, '1');
|
||||
if (needsVuMigration) localStorage.setItem(VU_OFFSET_ZERO_MIGRATION_KEY, '1');
|
||||
} catch (_) {}
|
||||
|
||||
if (!Number.isFinite(CONFIG.LUFS_I_WINDOW_MIN)) CONFIG.LUFS_I_WINDOW_MIN = CONFIG_DEFAULTS.LUFS_I_WINDOW_MIN ?? 4;
|
||||
CONFIG.LUFS_I_WINDOW_MIN = Math.max(1, Math.min(10, Math.round(CONFIG.LUFS_I_WINDOW_MIN)));
|
||||
CONFIG.RMS_TC_MODE = (CONFIG.RMS_TC_MODE === 'slow' || CONFIG.RMS_TC_MODE === 'window')
|
||||
? CONFIG.RMS_TC_MODE
|
||||
: 'fast';
|
||||
|
||||
CONFIG.LUFS_I_NORM_ENABLED = !!CONFIG.LUFS_I_NORM_ENABLED;
|
||||
CONFIG.STOPWATCH_DISPLAY_STYLE = (CONFIG.STOPWATCH_DISPLAY_STYLE === 'seven') ? 'seven' : 'mono';
|
||||
@@ -876,7 +876,6 @@ function loadConfig(opts = {}) {
|
||||
if (!Number.isFinite(CONFIG.LR_FRACTIONAL_DELAY_SAMPLES)) CONFIG.LR_FRACTIONAL_DELAY_SAMPLES = CONFIG_DEFAULTS.LR_FRACTIONAL_DELAY_SAMPLES;
|
||||
CONFIG.LR_FRACTIONAL_DELAY_SAMPLES = Math.max(-1.5, Math.min(1.5, Number(CONFIG.LR_FRACTIONAL_DELAY_SAMPLES)));
|
||||
CONFIG.PHOENIX_BASE_URL = normalizePhoenixBaseUrlForCurrentClient(CONFIG.PHOENIX_BASE_URL || CONFIG_DEFAULTS.PHOENIX_BASE_URL || DEFAULT_PHOENIX_BASE_URL);
|
||||
CONFIG.PPM_DIN_FAST_ATTACK = !!CONFIG.PPM_DIN_FAST_ATTACK;
|
||||
// Migrate stale profiles: detector ballistics are standards profiles,
|
||||
// not user-adjustable display preferences.
|
||||
CONFIG.PPM_DIN_ATTACK_MS = 10;
|
||||
@@ -1055,6 +1054,12 @@ function buildPhoenixGlobalConfigPayload() {
|
||||
return (mode === '1_3' || mode === '1_6' || mode === '1_12') ? mode : '1_6';
|
||||
};
|
||||
const phaseAmplitudeMode = normalizePhaseAmplitudeMode(CONFIG.PHASE_AMPLITUDE_MODE);
|
||||
const rmsIntegration = (() => {
|
||||
const mode = String(CONFIG.RMS_TC_MODE || '').trim().toLowerCase();
|
||||
if (mode === 'slow' || mode === 'window') return mode;
|
||||
if (mode === 'none' || mode === 'window300') return 'window';
|
||||
return 'fast';
|
||||
})();
|
||||
return {
|
||||
fftSize,
|
||||
rtaBpoMode: normalizeRtaBpoMode(CONFIG.RTA_BPO_MODE),
|
||||
@@ -1065,9 +1070,9 @@ function buildPhoenixGlobalConfigPayload() {
|
||||
lrFractionalDelaySamples: Number.isFinite(CONFIG.LR_FRACTIONAL_DELAY_SAMPLES) ? CONFIG.LR_FRACTIONAL_DELAY_SAMPLES : (CONFIG_DEFAULTS.LR_FRACTIONAL_DELAY_SAMPLES ?? 0.996),
|
||||
ppmDinAttackMs: 10,
|
||||
ppmDinDecayDbPerS: 20 / 1.5,
|
||||
ppmDinFastAttack: !!CONFIG.PPM_DIN_FAST_ATTACK,
|
||||
ppmEbuAttackMs: 10,
|
||||
ppmEbuDecayDbPerS: 24 / 2.8,
|
||||
rmsIntegration,
|
||||
lufsIWindowMin: Number.isFinite(CONFIG.LUFS_I_WINDOW_MIN) ? CONFIG.LUFS_I_WINDOW_MIN : (CONFIG_DEFAULTS.LUFS_I_WINDOW_MIN ?? 4),
|
||||
lufsINormEnabled: !!CONFIG.LUFS_I_NORM_ENABLED,
|
||||
ppmDinLoudnessBoxes: !!CONFIG.PPM_DIN_LOUDNESS_BOXES,
|
||||
@@ -1162,9 +1167,12 @@ function applyPhoenixGlobalConfig(payload = {}) {
|
||||
if ('spectroScrollMode' in payload) CONFIG.SPECTRO_SCROLL_MODE = normalizeSpectroScroll(payload.spectroScrollMode);
|
||||
CONFIG.PPM_DIN_ATTACK_MS = 10;
|
||||
CONFIG.PPM_DIN_DECAY_DB_PER_S = 20 / 1.5;
|
||||
if ('ppmDinFastAttack' in payload) CONFIG.PPM_DIN_FAST_ATTACK = !!payload.ppmDinFastAttack;
|
||||
CONFIG.PPM_EBU_ATTACK_MS = 10;
|
||||
CONFIG.PPM_EBU_DECAY_DB_PER_S = 24 / 2.8;
|
||||
if ('rmsIntegration' in payload) {
|
||||
const mode = String(payload.rmsIntegration || '').trim().toLowerCase();
|
||||
CONFIG.RMS_TC_MODE = (mode === 'slow' || mode === 'window') ? mode : 'fast';
|
||||
}
|
||||
const lufsWin = Number(payload.lufsIWindowMin);
|
||||
if (Number.isFinite(lufsWin)) CONFIG.LUFS_I_WINDOW_MIN = Math.max(1, Math.min(10, Math.round(lufsWin)));
|
||||
if ('lufsINormEnabled' in payload) CONFIG.LUFS_I_NORM_ENABLED = !!payload.lufsINormEnabled;
|
||||
@@ -1297,7 +1305,7 @@ function applyBroadcastStandard(name) {
|
||||
CONFIG.VU_RED_START = p.VU_RED_START;
|
||||
|
||||
// Offsets zurücksetzen (wie bisher)
|
||||
CONFIG.VU_OFFSET_DB = VU_OFFSET_ALIGNMENT_DB;
|
||||
CONFIG.VU_OFFSET_DB = VU_OFFSET_DEFAULT_DB;
|
||||
CONFIG.TP_OFFSET_DB = 0;
|
||||
CONFIG.RMS_OFFSET_DB = 0;
|
||||
|
||||
|
||||
+7
-26
@@ -1052,13 +1052,6 @@
|
||||
</label>
|
||||
<small>Dokument‑Variante (PML=+6 dBu; AL=0 dBu → −6 dB). Deaktiviert: AL → −9 dB.</small>
|
||||
</div>
|
||||
<div class="opt">
|
||||
<label class="row">
|
||||
<input id="opt_ppmDinFastAttack" type="checkbox">
|
||||
PPM DIN: Fast‑Attack (sofort, nicht normgerecht)
|
||||
</label>
|
||||
<small>Umgeht die feste 10-ms-Normintegration und zeigt Sample-Peaks sofort an (kann gefühltes Anzeige-Lag reduzieren).</small>
|
||||
</div>
|
||||
<div class="opt">
|
||||
<label class="row" style="align-items:center;gap:8px">
|
||||
<input id="opt_ppmDinHeaderValue" type="checkbox">
|
||||
@@ -1170,8 +1163,8 @@
|
||||
</label>
|
||||
<small>Blendet „RMS …“ aus und zeigt oben den aktuellen Wert</small>
|
||||
</div>
|
||||
<div class="opt"><label>RMS: Rot ab (dBFS)</label>
|
||||
<input id="opt_rmsRedThr" type="number" min="-40" max="0" step="0.5" style="width:110px">
|
||||
<div class="opt"><label>RMS: Rot ab (Anzeigeeinheit)</label>
|
||||
<input id="opt_rmsRedThr" type="number" min="-60" max="24" step="0.5" style="width:110px">
|
||||
<label class="row"><input id="opt_rmsRedBarOnly" type="checkbox"> nur roter Teil</label>
|
||||
</div>
|
||||
<div class="opt"><label>RMS Farben</label>
|
||||
@@ -1182,10 +1175,9 @@
|
||||
<select id="opt_rmsTc" style="width:170px">
|
||||
<option value="fast">Fast (125 ms)</option>
|
||||
<option value="slow">Slow (1000 ms)</option>
|
||||
<option value="impulse">Impulse (35 ms)</option>
|
||||
<option value="none">Keine (instant)</option>
|
||||
<option value="window">Fenster (300 ms)</option>
|
||||
</select>
|
||||
<small>IEC 60268: Impulse 35 ms, Fast 125 ms, Slow 1000 ms</small>
|
||||
<small>Integration der linearen Signalleistung im Audiokern. Fast/Slow verwenden 125 ms beziehungsweise 1 s; Fenster misst gleitend über exakt 300 ms.</small>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
@@ -1279,7 +1271,7 @@
|
||||
<li><strong>Echtzeit-Datenweg:</strong> WebSocket-Verarbeitung auf „latest value wins“ umgestellt. Messwerte, Spektrogramm sowie Goniometer/Waveform verwenden getrennte, begrenzte Datenwege; alte Frames können keine anwachsende Anzeigeverzögerung mehr bilden.</li>
|
||||
<li><strong>Browserlast:</strong> Single-Slot-Puffer und Sequenzprüfung für Mess-, Spektrogramm- und Visualisierungsdaten ergänzt. Große Rohsamplefelder werden nicht mehr in jedem JSON-Messpaket wiederholt.</li>
|
||||
<li><strong>Spektrogramm:</strong> Langzeitstillstand und stotterndes Nachholen beseitigt. Inkrementelles Spaltenzeichnen, Worker-ACK, Watchdog/Neustart und begrenztes Überspringen veralteter Spalten ergänzt. 0,5×, 1×, 2×, 4× und 6× besitzen nun eine feste, FFT- und DPI-unabhängige Zeitbasis.</li>
|
||||
<li><strong>DIN-/EBU-PPM:</strong> Blockunabhängige Quasi-Peak-Detektoren mit bandbegrenzter Interpolation, korrekter Attack- und Rücklaufballistik sowie automatischen Tonburst-, Frequenzgang- und Polaritätstests implementiert. Eine zweite Browser-Anstiegsballistik wurde entfernt.</li>
|
||||
<li><strong>DIN-/EBU-PPM:</strong> Blockunabhängige Quasi-Peak-Detektoren mit bandbegrenzter Interpolation, korrekter Attack- und Rücklaufballistik sowie automatischen Tonburst-, Frequenzgang- und Polaritätstests implementiert. Eine zweite Browser-Anstiegsballistik und der alte nicht normgerechte DIN-Fast-Attack-Sonderweg wurden vollständig entfernt.</li>
|
||||
<li><strong>RTW-RTA:</strong> RTW-Profil verbindlich auf die IIR-Oktavteilband-Filterbank festgelegt. Bis zur Vergleichsmessung am realen RTW-Gerät sind die bisherige Phoenix/RTW-Charakteristik und vollständige Butterworth-Bandpässe sechster Ordnung getrennt auswählbar.</li>
|
||||
<li><strong>RTA-Detektor und Reaktionszeit:</strong> Average (RMS) und Peak (gleitendes 10-ms-Maximum) sind nun getrennt von Fast, Medium, Slow und Impulse wählbar. Fast wurde auf 125 ms korrigiert; Impulse verwendet 35 ms Anstieg und 1,5 s Rücklauf. Die IIR-Detektoren arbeiten samplegenau und unabhängig von der ALSA-Periodengröße. Alte Einstellungen werden automatisch migriert.</li>
|
||||
<li><strong>RTA-Auflösung:</strong> Umschaltung zwischen 1/3, 1/6 und 1/12 repariert. Alle drei Auflösungen bleiben im RTW-Profil aktiv und verwenden dessen IIR-Filterbank, Darstellung und Ballistik.</li>
|
||||
@@ -1287,7 +1279,8 @@
|
||||
<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>True Peak, RMS und VU:</strong> True-Peak-Interpolation läuft nun lückenlos über Capture-Blockgrenzen; ein festes gleitendes 300-ms-Leistungsfenster ersetzt das periodengrößenabhängige Block-RMS. VU verwendet statt Rechteckmittelung ein Moving-Coil-Modell mit 300-ms-Sprungantwort und 1 bis 1,5 % Überschwingen.</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>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -1529,7 +1522,6 @@
|
||||
<p style="margin:0; color:#8fd3d4;">Veröffentlichung: 04.03.2026</p>
|
||||
<p style="margin:4px 0 0 12px; color:#8fd3d4;">Änderungen:</p>
|
||||
<ul style="margin:2px 0 0 24px; color:#b2c7d9;">
|
||||
<li>PPM DIN: Neue Option „Fast‑Attack“ (sofortes Anlaufen, nicht normgerecht) inkl. Hinweis‑Popup beim Aktivieren; Ballistik wird live aktualisiert.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1896,17 +1888,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="ppmDinFastWarn" class="rec-warning" style="display:none">
|
||||
<div class="rec-warning-box">
|
||||
<h3>PPM DIN – Fast‑Attack</h3>
|
||||
<p>Dieser Sample-Peak-Sondermodus lässt das PPM DIN sofort ansteigen (ohne 10-ms-Normintegration).</p>
|
||||
<p>Dadurch kann die Anzeige „schneller“ wirken, ist aber <b>nicht mehr normgerecht</b> (DIN 45406 / IEC 60268‑10).</p>
|
||||
<p>Hinweis: Das ändert nur die Anzeige‑Ballistik und kompensiert keine echte Audio‑Latenz.</p>
|
||||
<button id="ppmDinFastWarnBtn" class="btn btn-lg" type="button">Verstanden</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="presetSaveRemoteNotice" class="rec-warning" style="display:none">
|
||||
<div class="rec-warning-box">
|
||||
<h3>Preset hier nicht speichern</h3>
|
||||
|
||||
@@ -1700,6 +1700,11 @@ const audioState = {
|
||||
xyL: null,
|
||||
xyR: null,
|
||||
xySeq: 0,
|
||||
phaseAngleRad: null,
|
||||
phaseCoherence: 0,
|
||||
phaseLevel: 0,
|
||||
phasePeak: 0,
|
||||
phaseSeq: 0,
|
||||
rtaData: null,
|
||||
rmsDb: { L: -120, R: -120, mono: -120 },
|
||||
updateRtaConfig: null,
|
||||
|
||||
+12
-67
@@ -15,32 +15,25 @@ import { drawCachedStaticLayer } from './static_layer.js';
|
||||
import { METER_HEADER_FONT } from './scale_helpers.js';
|
||||
import { HEADER_BG, LABEL_COLOR, OK_COLOR, WARN_COLOR } from '../core/theme.js';
|
||||
|
||||
const RMS_FRAME_MS_NOMINAL = 16; // ~60 Hz UI-Refresh
|
||||
const RMS_FRAME_MS_MAX = 40; // Obergrenze für dt in der Glättung
|
||||
const RMS_FRAME_MS_SKIP = 250; // Heuristischer Schutz: riesige Gaps komplett skippen
|
||||
|
||||
export const id = 'rms';
|
||||
|
||||
export function initShared(CONFIG = {}) {
|
||||
const offset = CONFIG.RMS_OFFSET_DB || 0;
|
||||
const initVal = -60 + offset;
|
||||
const initVal = -60;
|
||||
return {
|
||||
values: { L: initVal, R: initVal }, // intern immer dBFS RMS aus der Messkette
|
||||
offset,
|
||||
values: { L: initVal, R: initVal }, // unveränderte dBFS-RMS-Werte aus dem Backend
|
||||
offset: 0,
|
||||
lastValidL: initVal,
|
||||
lastValidR: initVal,
|
||||
_smooth: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function update(packet, shared) {
|
||||
const offset = shared.offset || 0;
|
||||
const floor = -60 + offset;
|
||||
const floor = -60;
|
||||
if (!Number.isFinite(shared.lastValidL)) shared.lastValidL = floor;
|
||||
if (!Number.isFinite(shared.lastValidR)) shared.lastValidR = floor;
|
||||
|
||||
if (Number.isFinite(packet?.rmsL)) {
|
||||
const vL = packet.rmsL + offset;
|
||||
const vL = packet.rmsL;
|
||||
shared.values.L = vL;
|
||||
shared.lastValidL = vL;
|
||||
} else {
|
||||
@@ -48,7 +41,7 @@ export function update(packet, shared) {
|
||||
}
|
||||
|
||||
if (Number.isFinite(packet?.rmsR)) {
|
||||
const vR = packet.rmsR + offset;
|
||||
const vR = packet.rmsR;
|
||||
shared.values.R = vR;
|
||||
shared.lastValidR = vR;
|
||||
} else {
|
||||
@@ -65,6 +58,7 @@ export function draw(g, rect, CONFIG = {}, shared) {
|
||||
|
||||
// Anzeige-Umschaltung & Skala
|
||||
const isDBU = (MODE === 'dbu');
|
||||
const offset = Number.isFinite(CONFIG.RMS_OFFSET_DB) ? CONFIG.RMS_OFFSET_DB : 0;
|
||||
const LOG_MIN = -60;
|
||||
const LOG_TOP = isDBU ? +24 : 0;
|
||||
|
||||
@@ -120,35 +114,9 @@ export function draw(g, rect, CONFIG = {}, shared) {
|
||||
g.fillRect(rect.x, rect.y - 24, rect.w, 24);
|
||||
g.restore();
|
||||
|
||||
// IEC-ähnliche Zeitkonstanten (Impulse/Fast/Slow) für die Anzeige
|
||||
const tauMs = resolveRmsTau(CONFIG);
|
||||
if (tauMs > 0) {
|
||||
const now = performance.now();
|
||||
if (!shared._smooth) {
|
||||
shared._smooth = {
|
||||
L: shared.values.L,
|
||||
R: shared.values.R,
|
||||
lastTs: now - RMS_FRAME_MS_NOMINAL,
|
||||
};
|
||||
}
|
||||
const lastTs = shared._smooth.lastTs ?? (now - RMS_FRAME_MS_NOMINAL);
|
||||
const dtRawMs = Math.max(0, now - lastTs);
|
||||
shared._smooth.lastTs = now;
|
||||
|
||||
if (dtRawMs <= RMS_FRAME_MS_SKIP) {
|
||||
const dtUsedMs = Math.min(dtRawMs, RMS_FRAME_MS_MAX); // clamp dt to ignore occasional large gaps
|
||||
const alpha = 1 - Math.exp(-dtUsedMs / tauMs);
|
||||
shared._smooth.L += alpha * (shared.values.L - shared._smooth.L);
|
||||
shared._smooth.R += alpha * (shared.values.R - shared._smooth.R);
|
||||
}
|
||||
} else {
|
||||
shared._smooth = null;
|
||||
}
|
||||
|
||||
// Werte in Anzeigeeinheit clampen
|
||||
const rawDispL = toDisplay(shared._smooth?.L ?? shared.values.L);
|
||||
const rawDispR = toDisplay(shared._smooth?.R ?? shared.values.R);
|
||||
const smooth = smoothHeader(shared, rawDispL, rawDispR);
|
||||
const rawDispL = toDisplay(shared.values.L + offset);
|
||||
const rawDispR = toDisplay(shared.values.R + offset);
|
||||
const vL = clamp(rawDispL, LOG_MIN, LOG_TOP);
|
||||
const vR = clamp(rawDispR, LOG_MIN, LOG_TOP);
|
||||
g.save();
|
||||
@@ -167,11 +135,11 @@ export function draw(g, rect, CONFIG = {}, shared) {
|
||||
const isRedL = rawDispL > RED_START;
|
||||
const isRedR = rawDispR > RED_START;
|
||||
g.fillStyle = isRedL ? WARN_COLOR : (CONFIG.HEADER_TEXT_COLOR || MID_COLOR);
|
||||
g.fillText(fmt(smooth.L), centerLeft, yText);
|
||||
g.fillText(fmt(rawDispL), centerLeft, yText);
|
||||
g.fillStyle = CONFIG.HEADER_TEXT_COLOR || MID_COLOR;
|
||||
g.fillText('|', centerX, yText);
|
||||
g.fillStyle = isRedR ? WARN_COLOR : (CONFIG.HEADER_TEXT_COLOR || MID_COLOR);
|
||||
g.fillText(fmt(smooth.R), centerRight, yText);
|
||||
g.fillText(fmt(rawDispR), centerRight, yText);
|
||||
} else {
|
||||
const title = isDBU ? 'RMS (dBu)' : 'RMS (dBFS RMS)';
|
||||
g.fillText(title, centerX, rect.y - 12);
|
||||
@@ -284,7 +252,7 @@ function drawRmsStaticOverlay(g, shared, rect, CONFIG, geom) {
|
||||
bottomValue: LOG_MIN,
|
||||
highlightTick: null,
|
||||
});
|
||||
const unitLabel = 'dBFS (RMS)';
|
||||
const unitLabel = isDBU ? 'dBu (RMS)' : 'dBFS (RMS)';
|
||||
cg.fillStyle = LABEL_COLOR;
|
||||
cg.textAlign = 'center';
|
||||
const baseY = rect.y + rect.h + 16;
|
||||
@@ -303,19 +271,6 @@ function drawRmsStaticOverlay(g, shared, rect, CONFIG, geom) {
|
||||
|
||||
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
|
||||
|
||||
function resolveRmsTau(CONFIG = {}) {
|
||||
const mode = String(CONFIG.RMS_TC_MODE || 'fast').toLowerCase();
|
||||
const custom = Number(CONFIG.RMS_TC_MS);
|
||||
if (Number.isFinite(custom) && custom > 0) return custom;
|
||||
switch (mode) {
|
||||
case 'impulse': return 35;
|
||||
case 'slow': return 1000;
|
||||
case 'none': return 0;
|
||||
case 'fast':
|
||||
default: return 125;
|
||||
}
|
||||
}
|
||||
|
||||
function drawRedStripeLR(g, leftX, barW, rightX, centerX, mapY, yWarn, yTop) {
|
||||
const innerRight = leftX + barW;
|
||||
const gapL = Math.abs(centerX - innerRight);
|
||||
@@ -491,13 +446,3 @@ function getRmsAlignmentHighlight(CONFIG) {
|
||||
color: WARN_COLOR,
|
||||
};
|
||||
}
|
||||
|
||||
function smoothHeader(shared, rawL, rawR, alpha = 0.2) {
|
||||
if (!shared._header) {
|
||||
shared._header = { L: rawL, R: rawR };
|
||||
return shared._header;
|
||||
}
|
||||
shared._header.L += alpha * (rawL - shared._header.L);
|
||||
shared._header.R += alpha * (rawR - shared._header.R);
|
||||
return shared._header;
|
||||
}
|
||||
|
||||
+34
-34
@@ -1,25 +1,12 @@
|
||||
// meters/tp.js — True Peak (dBTP) L/R mit nichtlinearer Skala, Warnschwelle und Glanzkante
|
||||
// meters/tp.js — True Peak (dBTP) L/R mit Übersteuerungsreserve bis +6 dBTP
|
||||
import { createPeakHoldState, stepPeakHold } from '../core/utils.js';
|
||||
import { drawHairlineGrid, METER_HEADER_FONT } from './scale_helpers.js';
|
||||
import { drawCachedStaticLayer } from './static_layer.js';
|
||||
import { TRUE_PEAK_MINOR_TICKS, TRUE_PEAK_SCALE } from './true_peak_scale.js';
|
||||
import { HEADER_BG, LABEL_COLOR, MID_COLOR, WARN_COLOR } from '../core/theme.js';
|
||||
|
||||
export const id = 'tp';
|
||||
|
||||
const TP_PERCENT_SCALE = [
|
||||
{ db: -60, percent: '0,00 %', frac: 0.0000 },
|
||||
{ db: -50, percent: '16,67 %', frac: 0.0373 },
|
||||
{ db: -40, percent: '33,33 %', frac: 0.1429 },
|
||||
{ db: -35, percent: '41,67 %', frac: 0.2112 },
|
||||
{ db: -30, percent: '50,00 %', frac: 0.2857 },
|
||||
{ db: -25, percent: '58,33 %', frac: 0.3851 },
|
||||
{ db: -20, percent: '66,67 %', frac: 0.4783 },
|
||||
{ db: -15, percent: '75,00 %', frac: 0.6087 },
|
||||
{ db: -10, percent: '83,33 %', frac: 0.7391 },
|
||||
{ db: -5, percent: '91,67 %', frac: 0.8696 },
|
||||
{ db: 0, percent: '100,00 %', frac: 1.0000 },
|
||||
];
|
||||
|
||||
export function initShared(CONFIG) {
|
||||
const now = performance.now();
|
||||
return {
|
||||
@@ -42,22 +29,22 @@ export function update(packet, shared) {
|
||||
shared.values.R = R + (shared.offset || 0);
|
||||
}
|
||||
|
||||
const LOG_MIN = TP_PERCENT_SCALE[0].db;
|
||||
const LOG_TOP = TP_PERCENT_SCALE[TP_PERCENT_SCALE.length - 1].db;
|
||||
const LOG_MIN = TRUE_PEAK_SCALE[0].db;
|
||||
const LOG_TOP = TRUE_PEAK_SCALE[TRUE_PEAK_SCALE.length - 1].db;
|
||||
|
||||
function interpolateFrac(db) {
|
||||
if (db <= TP_PERCENT_SCALE[0].db) return TP_PERCENT_SCALE[0].frac;
|
||||
if (db >= TP_PERCENT_SCALE[TP_PERCENT_SCALE.length - 1].db) return TP_PERCENT_SCALE[TP_PERCENT_SCALE.length - 1].frac;
|
||||
for (let i = 1; i < TP_PERCENT_SCALE.length; i++) {
|
||||
const prev = TP_PERCENT_SCALE[i - 1];
|
||||
const curr = TP_PERCENT_SCALE[i];
|
||||
if (db <= TRUE_PEAK_SCALE[0].db) return TRUE_PEAK_SCALE[0].frac;
|
||||
if (db >= TRUE_PEAK_SCALE[TRUE_PEAK_SCALE.length - 1].db) return TRUE_PEAK_SCALE[TRUE_PEAK_SCALE.length - 1].frac;
|
||||
for (let i = 1; i < TRUE_PEAK_SCALE.length; i++) {
|
||||
const prev = TRUE_PEAK_SCALE[i - 1];
|
||||
const curr = TRUE_PEAK_SCALE[i];
|
||||
if (db <= curr.db) {
|
||||
const span = curr.db - prev.db || 1;
|
||||
const t = (db - prev.db) / span;
|
||||
return prev.frac + t * (curr.frac - prev.frac);
|
||||
}
|
||||
}
|
||||
return TP_PERCENT_SCALE[TP_PERCENT_SCALE.length - 1].frac;
|
||||
return TRUE_PEAK_SCALE[TRUE_PEAK_SCALE.length - 1].frac;
|
||||
}
|
||||
|
||||
export function draw(g, rect, CONFIG, shared) {
|
||||
@@ -122,13 +109,30 @@ export function draw(g, rect, CONFIG, shared) {
|
||||
g.font = prevFont;
|
||||
g.restore();
|
||||
const yFloor = mapY(LOG_MIN);
|
||||
const configuredRedStart = Number(CONFIG.TP_RED_START);
|
||||
const redStart = Math.max(LOG_MIN, Math.min(LOG_TOP, Number.isFinite(configuredRedStart) ? configuredRedStart : -1));
|
||||
const yRed = mapY(redStart);
|
||||
const innerW = Math.max(2, barW - 2);
|
||||
const colNorm = CONFIG.TP_COLOR_NORMAL || MID_COLOR;
|
||||
const colWarn = CONFIG.TP_COLOR_WARN || WARN_COLOR;
|
||||
const redOnly = CONFIG.TP_RED_BAR_ONLY !== false;
|
||||
|
||||
const drawBar = (x0, val) => {
|
||||
const yVal = mapY(val);
|
||||
const colNorm = CONFIG.TP_COLOR_NORMAL;
|
||||
g.fillStyle = colNorm;
|
||||
g.fillRect(x0 + 1, yVal, innerW, Math.max(0, yFloor - yVal));
|
||||
if (val > redStart) {
|
||||
if (redOnly) {
|
||||
g.fillStyle = colNorm;
|
||||
g.fillRect(x0 + 1, yRed, innerW, Math.max(0, yFloor - yRed));
|
||||
g.fillStyle = colWarn;
|
||||
g.fillRect(x0 + 1, yVal, innerW, Math.max(0, yRed - yVal));
|
||||
} else {
|
||||
g.fillStyle = colWarn;
|
||||
g.fillRect(x0 + 1, yVal, innerW, Math.max(0, yFloor - yVal));
|
||||
}
|
||||
} else {
|
||||
g.fillStyle = colNorm;
|
||||
g.fillRect(x0 + 1, yVal, innerW, Math.max(0, yFloor - yVal));
|
||||
}
|
||||
// Glanzkante
|
||||
g.globalAlpha = .12;
|
||||
g.fillStyle = '#fff';
|
||||
@@ -211,9 +215,6 @@ function drawTpStaticOverlay(g, shared, rect, CONFIG, geom, mapY) {
|
||||
});
|
||||
}
|
||||
|
||||
// Balken-Overlay-Ticks auf Basis der Vintage-Prozent-Skala
|
||||
const EXTRA_MINOR_TICKS = [-47, -19, -18, -17, -16, -14, -13, -12, -11, -9, -8, -7, -6, -4, -3, -2, -1];
|
||||
|
||||
function drawOverlayTicksLR(g, leftX, rightX, widthPx, mapY, alignmentTick = null) {
|
||||
if (!g) return;
|
||||
|
||||
@@ -254,14 +255,14 @@ function drawOverlayTicksLR(g, leftX, rightX, widthPx, mapY, alignmentTick = nul
|
||||
g.beginPath(); g.moveTo(x1R, yPix); g.lineTo(x2R, yPix); g.stroke();
|
||||
};
|
||||
|
||||
for (const point of TP_PERCENT_SCALE) {
|
||||
for (const point of TRUE_PEAK_SCALE) {
|
||||
const y = Math.round(mapY(point.db)) + 0.5;
|
||||
const isHighlight = highlightValue !== null && approx(point.db, highlightValue);
|
||||
if (isHighlight) highlightMatched = true;
|
||||
drawMajor(y, isHighlight);
|
||||
}
|
||||
|
||||
for (const db of EXTRA_MINOR_TICKS) {
|
||||
for (const db of TRUE_PEAK_MINOR_TICKS) {
|
||||
const y = Math.round(mapY(db)) + 0.5;
|
||||
const isHighlight = highlightValue !== null && approx(db, highlightValue);
|
||||
if (isHighlight) highlightMatched = true;
|
||||
@@ -289,11 +290,10 @@ function drawScale(g, colRect, centerX, mapY, CONFIG) {
|
||||
g.textAlign = 'center';
|
||||
g.textBaseline = 'alphabetic';
|
||||
|
||||
for (const point of TP_PERCENT_SCALE) {
|
||||
for (const point of TRUE_PEAK_SCALE) {
|
||||
const y = mapY(point.db);
|
||||
if (y < colRect.y || y > colRect.y + colRect.h) continue;
|
||||
const value = Math.abs(point.db);
|
||||
const dbLabel = Number.isInteger(value) ? String(value) : value.toFixed(1);
|
||||
const dbLabel = point.db > 0 ? `+${point.db}` : String(point.db);
|
||||
g.fillText(dbLabel, centerX, y + 5);
|
||||
}
|
||||
g.font = prevFont;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// Shared dBTP scale for every True-Peak presentation. The final 10% is kept
|
||||
// above 0 dBTP so intersample overloads remain measurable up to +6 dBTP.
|
||||
export const TRUE_PEAK_SCALE = Object.freeze([
|
||||
{ db: -60, frac: 0.0000 },
|
||||
{ db: -50, frac: 0.0336 },
|
||||
{ db: -40, frac: 0.1286 },
|
||||
{ db: -35, frac: 0.1901 },
|
||||
{ db: -30, frac: 0.2571 },
|
||||
{ db: -25, frac: 0.3466 },
|
||||
{ db: -20, frac: 0.4305 },
|
||||
{ db: -15, frac: 0.5478 },
|
||||
{ db: -10, frac: 0.6652 },
|
||||
{ db: -5, frac: 0.7826 },
|
||||
{ db: 0, frac: 0.9000 },
|
||||
{ db: +3, frac: 0.9500 },
|
||||
{ db: +6, frac: 1.0000 },
|
||||
]);
|
||||
|
||||
export const TRUE_PEAK_MINOR_TICKS = Object.freeze([
|
||||
-47, -19, -18, -17, -16, -14, -13, -12, -11, -9, -8, -7, -6, -4, -3, -2, -1,
|
||||
+1, +2, +4, +5,
|
||||
]);
|
||||
+2
-52
@@ -7,7 +7,6 @@ import { clamp, clampPow2 } from '../core/utils.js';
|
||||
// DOM helper
|
||||
const E = (id) => document.getElementById(id);
|
||||
const PPM_LOUD_WARN_KEY = 'ppm_din_loudness_warn_ack_v1';
|
||||
const PPM_DIN_FAST_WARN_KEY = 'ppm_din_fast_attack_warn_ack_v1';
|
||||
|
||||
function getPpmDinVisibleBaseOffset() {
|
||||
const baseMode = (CONFIG.PPM_DIN_MODE === 'al_minus6') ? -6 : -9;
|
||||
@@ -81,27 +80,6 @@ function showPpmLoudnessWarning(onConfirm) {
|
||||
wrap.style.display = 'flex';
|
||||
}
|
||||
|
||||
function showPpmDinFastAttackWarning(onConfirm) {
|
||||
const wrap = E('ppmDinFastWarn');
|
||||
const btn = E('ppmDinFastWarnBtn');
|
||||
if (!wrap || !btn) {
|
||||
if (onConfirm) onConfirm();
|
||||
return;
|
||||
}
|
||||
if (!wrap.dataset.bound) {
|
||||
btn.addEventListener('click', () => {
|
||||
try { localStorage.setItem(PPM_DIN_FAST_WARN_KEY, '1'); } catch (_) {}
|
||||
wrap.style.display = 'none';
|
||||
if (onConfirm) onConfirm();
|
||||
});
|
||||
wrap.addEventListener('click', (e) => {
|
||||
if (e.target === wrap) wrap.style.display = 'none';
|
||||
});
|
||||
wrap.dataset.bound = '1';
|
||||
}
|
||||
wrap.style.display = 'flex';
|
||||
}
|
||||
|
||||
export function setupOptions(env) {
|
||||
// Initial laden
|
||||
loadConfig();
|
||||
@@ -237,7 +215,6 @@ function syncUI() {
|
||||
['opt_ppmDinColWarn', CONFIG.PPM_DIN_COLOR_WARN],
|
||||
['opt_ppmDinHeaderValue', CONFIG.PPM_DIN_HEADER_SHOW_VALUE, null, null, 'checkbox'],
|
||||
['opt_ppmDinLoudnessBox', CONFIG.PPM_DIN_LOUDNESS_BOXES, null, null, 'checkbox'],
|
||||
['opt_ppmDinFastAttack', CONFIG.PPM_DIN_FAST_ATTACK, null, null, 'checkbox'],
|
||||
['opt_ppmDinLoudOff', CONFIG.PPM_DIN_LOUDNESS_OFFSET_DB ?? 0, 'val_ppmDinLoudOff', (v)=>`${Number(v).toFixed(1)} dB`],
|
||||
['opt_ppmEbuColNorm', CONFIG.PPM_EBU_COLOR_NORMAL],
|
||||
['opt_ppmEbuColWarn', CONFIG.PPM_EBU_COLOR_WARN],
|
||||
@@ -544,7 +521,6 @@ function wireHandlers(env) {
|
||||
const notifyRta = /^opt_rta/i.test(id)
|
||||
|| id === 'opt_lufsIWindowMin'
|
||||
|| id === 'opt_lufsINorm';
|
||||
const notifyPpm = (id === 'opt_ppmDinFastAttack');
|
||||
const inputType = String(el.type || '').toLowerCase();
|
||||
const commitOnInput = isCheckbox || inputType === 'range' || inputType === 'color';
|
||||
|
||||
@@ -558,9 +534,6 @@ function wireHandlers(env) {
|
||||
if (notifyRta) {
|
||||
try { env?.notifyRtaConfig?.(); } catch (_) {}
|
||||
}
|
||||
if (notifyPpm) {
|
||||
try { env?.notifyPpmConfig?.(); } catch (_) {}
|
||||
}
|
||||
try { env?.syncRealtimeGain?.(); } catch (_) {}
|
||||
try { env?.syncWaveformWindow?.(); } catch (_) {}
|
||||
try { env?.syncWaveformMode?.(); } catch (_) {}
|
||||
@@ -789,29 +762,6 @@ function wireHandlers(env) {
|
||||
CONFIG.PPM_DIN_HEADER_SHOW_VALUE = !!v;
|
||||
return CONFIG.PPM_DIN_HEADER_SHOW_VALUE;
|
||||
}, null, null, true);
|
||||
h('opt_ppmDinFastAttack', v => {
|
||||
const desired = !!v;
|
||||
const cb = E('opt_ppmDinFastAttack');
|
||||
const acked = (() => {
|
||||
try { return localStorage.getItem(PPM_DIN_FAST_WARN_KEY) === '1'; }
|
||||
catch (_) { return false; }
|
||||
})();
|
||||
if (desired && !acked) {
|
||||
if (cb) cb.checked = false;
|
||||
CONFIG.PPM_DIN_FAST_ATTACK = false;
|
||||
showPpmDinFastAttackWarning(() => {
|
||||
CONFIG.PPM_DIN_FAST_ATTACK = true;
|
||||
if (cb) cb.checked = true;
|
||||
try { saveConfig(); } catch (_) {}
|
||||
try { env?.notifyPpmConfig?.(); } catch (_) {}
|
||||
notifyPhoenixGlobalConfig();
|
||||
});
|
||||
return false;
|
||||
}
|
||||
CONFIG.PPM_DIN_FAST_ATTACK = desired;
|
||||
notifyPhoenixGlobalConfig();
|
||||
return desired;
|
||||
}, null, null, true);
|
||||
h('opt_ppmDinLoudnessBox', v => {
|
||||
const desired = !!v;
|
||||
const cb = E('opt_ppmDinLoudnessBox');
|
||||
@@ -875,9 +825,9 @@ function wireHandlers(env) {
|
||||
}, null, null, true);
|
||||
h('opt_rmsTc', v => {
|
||||
const mode = String(v).toLowerCase();
|
||||
if (mode === 'impulse' || mode === 'fast' || mode === 'slow' || mode === 'none') {
|
||||
if (mode === 'fast' || mode === 'slow' || mode === 'window') {
|
||||
CONFIG.RMS_TC_MODE = mode;
|
||||
CONFIG.RMS_TC_MS = null;
|
||||
notifyPhoenixGlobalConfig();
|
||||
}
|
||||
return CONFIG.RMS_TC_MODE;
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Nutzt die bestehenden VU-Werte (inkl. Offsets/Kalibrierung) und zeichnet
|
||||
// zwei kompakte, horizontale Nadelinstrumente (L/R) mit Slot-Auswahl.
|
||||
import { drawCachedStaticLayer } from './static_layer.js';
|
||||
import { TRUE_PEAK_MINOR_TICKS, TRUE_PEAK_SCALE } from '../meters/true_peak_scale.js';
|
||||
|
||||
export const id = 'classic-needles';
|
||||
|
||||
@@ -47,22 +48,6 @@ const DIN_SCALE = [
|
||||
{ db: +5, pos: 1.0000 },
|
||||
];
|
||||
|
||||
const TP_PERCENT_SCALE = [
|
||||
{ db: -60, frac: 0.0000 },
|
||||
{ db: -50, frac: 0.0373 },
|
||||
{ db: -40, frac: 0.1429 },
|
||||
{ db: -35, frac: 0.2112 },
|
||||
{ db: -30, frac: 0.2857 },
|
||||
{ db: -25, frac: 0.3851 },
|
||||
{ db: -20, frac: 0.4783 },
|
||||
{ db: -15, frac: 0.6087 },
|
||||
{ db: -10, frac: 0.7391 },
|
||||
{ db: -5, frac: 0.8696 },
|
||||
{ db: 0, frac: 1.0000 },
|
||||
];
|
||||
|
||||
const TP_EXTRA_MINOR_TICKS = [-47, -19, -18, -17, -16, -14, -13, -12, -11, -9, -8, -7, -6, -4, -3, -2, -1];
|
||||
|
||||
const PPM_EBU_MAJOR_TICKS = [-12, -8, -4, 0, +4, +8, +12];
|
||||
const PPM_EBU_MINOR_TICKS = [-10, -6, -2, +2, +6, +9, +10];
|
||||
|
||||
@@ -122,7 +107,19 @@ export async function render(env, state) {
|
||||
const raw = readMeterDisplayLR(meterId, meterState, CONFIG, scale, audio);
|
||||
const targets = { L: scale.valueToNorm(raw.L), R: scale.valueToNorm(raw.R) };
|
||||
const now = (typeof performance !== 'undefined' ? performance.now() : Date.now());
|
||||
smoothNeedle(state, targets, now);
|
||||
if (meterId === 'rms') {
|
||||
// RMS ballistics are already measured sample-continuously in the backend.
|
||||
// A second spring model here would add view-dependent delay and values.
|
||||
state.smooth.L = targets.L;
|
||||
state.smooth.R = targets.R;
|
||||
state.goal.L = targets.L;
|
||||
state.goal.R = targets.R;
|
||||
state.vel.L = 0;
|
||||
state.vel.R = 0;
|
||||
state.lastTs = now;
|
||||
} else {
|
||||
smoothNeedle(state, targets, now);
|
||||
}
|
||||
// Box-Abmessungen an den Real-Time-Analyzer anlehnen (gleiche Offsets)
|
||||
const BOX_LEFT = 0;
|
||||
const BOX_TOP = Number.isFinite(env?.topInset) ? Number(env.topInset) : 70;
|
||||
@@ -425,24 +422,24 @@ function getNeedleScaleDescriptor(meterId, CONFIG) {
|
||||
|
||||
if (meterId === 'tp') {
|
||||
const bottom = -60;
|
||||
const top = 0;
|
||||
const top = 6;
|
||||
const redStart = Number.isFinite(CONFIG?.TP_RED_START) ? CONFIG.TP_RED_START : -1;
|
||||
const warnColor = CONFIG?.TP_COLOR_WARN || '#ff3b3b';
|
||||
const normalColor = CONFIG?.TP_COLOR_NORMAL || '#ffe066';
|
||||
const mapRaw = (db) => {
|
||||
const clamped = clamp(db, bottom, top);
|
||||
if (clamped <= TP_PERCENT_SCALE[0].db) return TP_PERCENT_SCALE[0].frac;
|
||||
if (clamped >= TP_PERCENT_SCALE[TP_PERCENT_SCALE.length - 1].db) return TP_PERCENT_SCALE[TP_PERCENT_SCALE.length - 1].frac;
|
||||
for (let i = 1; i < TP_PERCENT_SCALE.length; i++) {
|
||||
const prev = TP_PERCENT_SCALE[i - 1];
|
||||
const curr = TP_PERCENT_SCALE[i];
|
||||
if (clamped <= TRUE_PEAK_SCALE[0].db) return TRUE_PEAK_SCALE[0].frac;
|
||||
if (clamped >= TRUE_PEAK_SCALE[TRUE_PEAK_SCALE.length - 1].db) return TRUE_PEAK_SCALE[TRUE_PEAK_SCALE.length - 1].frac;
|
||||
for (let i = 1; i < TRUE_PEAK_SCALE.length; i++) {
|
||||
const prev = TRUE_PEAK_SCALE[i - 1];
|
||||
const curr = TRUE_PEAK_SCALE[i];
|
||||
if (clamped <= curr.db) {
|
||||
const span = curr.db - prev.db || 1;
|
||||
const t = (clamped - prev.db) / span;
|
||||
return prev.frac + t * (curr.frac - prev.frac);
|
||||
}
|
||||
}
|
||||
return TP_PERCENT_SCALE[TP_PERCENT_SCALE.length - 1].frac;
|
||||
return TRUE_PEAK_SCALE[TRUE_PEAK_SCALE.length - 1].frac;
|
||||
};
|
||||
const valueToNorm = (v) => clamp01(mapRaw(v));
|
||||
return {
|
||||
@@ -454,11 +451,10 @@ function getNeedleScaleDescriptor(meterId, CONFIG) {
|
||||
warnColor,
|
||||
normalColor,
|
||||
unitLabel: 'dBTP',
|
||||
majorTicks: TP_PERCENT_SCALE.map((p) => p.db),
|
||||
minorTicks: TP_EXTRA_MINOR_TICKS,
|
||||
majorTicks: TRUE_PEAK_SCALE.map((p) => p.db),
|
||||
minorTicks: TRUE_PEAK_MINOR_TICKS,
|
||||
formatMajor: (db) => {
|
||||
const v = Math.abs(db);
|
||||
return Number.isInteger(v) ? String(v) : v.toFixed(1);
|
||||
return db > 0 ? `+${db}` : String(db);
|
||||
},
|
||||
valueToNorm,
|
||||
};
|
||||
@@ -633,7 +629,7 @@ function readMeterDisplayLR(meterId, shared, CONFIG, scale, audio) {
|
||||
const isDBU = (mode === 'dbu');
|
||||
if (isDBU) {
|
||||
const refDbfs = Number.isFinite(CONFIG?.RMS_REF_DBFS_FOR_REF_DBU) ? CONFIG.RMS_REF_DBFS_FOR_REF_DBU : -18;
|
||||
const refDbu = Number.isFinite(CONFIG?.RMS_REF_DBU) ? CONFIG.RMS_REF_DBU : +4;
|
||||
const refDbu = Number.isFinite(CONFIG?.RMS_REF_DBU) ? CONFIG.RMS_REF_DBU : 0;
|
||||
rawL = (rawL - refDbfs) + refDbu;
|
||||
rawR = (rawR - refDbfs) + refDbu;
|
||||
}
|
||||
|
||||
+11
-57
@@ -1,6 +1,7 @@
|
||||
// views/peak_history.js - Meter-Verlauf (rechter Slot) mit RTA-Layout
|
||||
|
||||
import { FRAME_COLOR, GRID_MAJOR_COLOR, GRID_MINOR_COLOR, LABEL_COLOR, MID_COLOR, PANEL_BG, WARN_COLOR } from '../core/theme.js';
|
||||
import { TRUE_PEAK_SCALE } from '../meters/true_peak_scale.js';
|
||||
|
||||
const PLOT = { left: 43, top: 70, right: 0, bottom: 18 };
|
||||
const METER_WIDTH = 140;
|
||||
@@ -47,24 +48,10 @@ const DIN_SCALE = [
|
||||
{ db: +5, pos: 1.0000 },
|
||||
];
|
||||
|
||||
const TP_PERCENT_SCALE = [
|
||||
{ db: -60, frac: 0.0000 },
|
||||
{ db: -50, frac: 0.0373 },
|
||||
{ db: -40, frac: 0.1429 },
|
||||
{ db: -35, frac: 0.2112 },
|
||||
{ db: -30, frac: 0.2857 },
|
||||
{ db: -25, frac: 0.3851 },
|
||||
{ db: -20, frac: 0.4783 },
|
||||
{ db: -15, frac: 0.6087 },
|
||||
{ db: -10, frac: 0.7391 },
|
||||
{ db: -5, frac: 0.8696 },
|
||||
{ db: 0, frac: 1.0000 },
|
||||
];
|
||||
|
||||
const VU_TICKS = [-20, -10, -7, -5, -3, 0, 1, 2, 3];
|
||||
const PPM_DIN_TICKS = [-50, -40, -30, -20, -10, -5, 0, 5];
|
||||
const PPM_EBU_TICKS = [-12, -8, -4, 0, 4, 8, 12];
|
||||
const TP_TICKS = [-60, -50, -40, -30, -20, -10, 0];
|
||||
const TP_TICKS = [-60, -50, -40, -30, -20, -10, 0, 3, 6];
|
||||
const RMS_DBFS_TICKS = [0, -6, -12, -18, -24, -30, -40, -60];
|
||||
const RMS_DBU_TICKS = [24, 20, 10, 0, -10, -20, -30, -40, -50, -60];
|
||||
const LUFS_TICKS = [-50, -40, -30, -23, -18, -10, -5];
|
||||
@@ -91,7 +78,6 @@ export function init() {
|
||||
meterLabel: '',
|
||||
neutralNorm: 0,
|
||||
neutralValue: 0,
|
||||
rmsSmooth: null,
|
||||
staticLayer: null,
|
||||
};
|
||||
}
|
||||
@@ -605,7 +591,6 @@ async function drawMeter(env, plotX, plotY, plotW, plotH, meterRects = null) {
|
||||
function resolveMeterInfo(env, state, meterId) {
|
||||
const cfg = env.config || {};
|
||||
const shared = env.meters?.getState?.(meterId) || null;
|
||||
if (meterId !== 'rms') state.rmsSmooth = null;
|
||||
|
||||
switch (meterId) {
|
||||
case 'ppm-din':
|
||||
@@ -615,7 +600,7 @@ function resolveMeterInfo(env, state, meterId) {
|
||||
case 'tp':
|
||||
return sampleTp(cfg, shared);
|
||||
case 'rms':
|
||||
return sampleRms(cfg, shared, state);
|
||||
return sampleRms(cfg, shared);
|
||||
case 'lufs':
|
||||
return sampleLufs(cfg, shared);
|
||||
case 'vu':
|
||||
@@ -715,8 +700,8 @@ function samplePpmEbu(cfg, shared) {
|
||||
}
|
||||
|
||||
function sampleTp(cfg, shared) {
|
||||
const bottom = TP_PERCENT_SCALE[0].db;
|
||||
const top = TP_PERCENT_SCALE[TP_PERCENT_SCALE.length - 1].db;
|
||||
const bottom = TRUE_PEAK_SCALE[0].db;
|
||||
const top = TRUE_PEAK_SCALE[TRUE_PEAK_SCALE.length - 1].db;
|
||||
const effOff = Number(cfg?.TP_OFFSET_DB) || 0;
|
||||
const offCorr = effOff - (shared?.offset || 0);
|
||||
const warnVal = Number.isFinite(cfg?.TP_RED_START) ? cfg.TP_RED_START : -1;
|
||||
@@ -740,21 +725,21 @@ function sampleTp(cfg, shared) {
|
||||
});
|
||||
}
|
||||
|
||||
function sampleRms(cfg, shared, state) {
|
||||
function sampleRms(cfg, shared) {
|
||||
const mode = String(cfg?.RMS_MODE || 'dbfs').toLowerCase();
|
||||
const isDBU = mode === 'dbu';
|
||||
const bottom = -60;
|
||||
const top = isDBU ? 24 : 0;
|
||||
const refDbfs = Number.isFinite(cfg?.RMS_REF_DBFS_FOR_REF_DBU) ? cfg.RMS_REF_DBFS_FOR_REF_DBU : -18;
|
||||
const refDbu = Number.isFinite(cfg?.RMS_REF_DBU) ? cfg.RMS_REF_DBU : 0;
|
||||
const offset = Number.isFinite(cfg?.RMS_OFFSET_DB) ? cfg.RMS_OFFSET_DB : 0;
|
||||
const warnVal = Number.isFinite(cfg?.RMS_RED_START)
|
||||
? cfg.RMS_RED_START
|
||||
: (isDBU ? 20 : 0);
|
||||
const vL = Number.isFinite(shared?.values?.L) ? shared.values.L : bottom;
|
||||
const vR = Number.isFinite(shared?.values?.R) ? shared.values.R : bottom;
|
||||
const base = Math.max(vL, vR);
|
||||
const smoothed = applyRmsSmoothing(state, base, cfg);
|
||||
const display = isDBU ? (smoothed - refDbfs + refDbu) : smoothed;
|
||||
const base = Math.max(vL, vR) + offset;
|
||||
const display = isDBU ? (base - refDbfs + refDbu) : base;
|
||||
const val = clamp(display, bottom, top);
|
||||
const toNorm = (db) => {
|
||||
const c = clamp(db, bottom, top);
|
||||
@@ -778,43 +763,12 @@ function sampleRms(cfg, shared, state) {
|
||||
toNorm,
|
||||
range: { bottom, top },
|
||||
ticks: isDBU ? RMS_DBU_TICKS : RMS_DBFS_TICKS,
|
||||
signature: ['rms', mode, refDbfs, refDbu, cfg?.RMS_TC_MODE, cfg?.RMS_TC_MS].join('|'),
|
||||
signature: ['rms', mode, refDbfs, refDbu, offset, cfg?.RMS_TC_MODE].join('|'),
|
||||
hasValue: !!shared,
|
||||
warnValue: warnVal,
|
||||
});
|
||||
}
|
||||
|
||||
function applyRmsSmoothing(state, value, cfg) {
|
||||
const tauMs = resolveRmsTau(cfg);
|
||||
if (!Number.isFinite(tauMs) || tauMs <= 0) {
|
||||
state.rmsSmooth = null;
|
||||
return value;
|
||||
}
|
||||
const now = performance.now();
|
||||
if (!state.rmsSmooth || !Number.isFinite(state.rmsSmooth.value)) {
|
||||
state.rmsSmooth = { value, lastTs: now };
|
||||
return value;
|
||||
}
|
||||
const dt = Math.max(1 / 240, (now - (state.rmsSmooth.lastTs || now)) / 1000);
|
||||
state.rmsSmooth.lastTs = now;
|
||||
const alpha = 1 - Math.exp(-dt / (tauMs / 1000));
|
||||
state.rmsSmooth.value += alpha * (value - state.rmsSmooth.value);
|
||||
return state.rmsSmooth.value;
|
||||
}
|
||||
|
||||
function resolveRmsTau(cfg = {}) {
|
||||
const mode = String(cfg.RMS_TC_MODE || 'fast').toLowerCase();
|
||||
const custom = Number(cfg.RMS_TC_MS);
|
||||
if (Number.isFinite(custom) && custom > 0) return custom;
|
||||
switch (mode) {
|
||||
case 'impulse': return 35;
|
||||
case 'slow': return 1000;
|
||||
case 'none': return 0;
|
||||
case 'fast':
|
||||
default: return 125;
|
||||
}
|
||||
}
|
||||
|
||||
function sampleLufs(cfg, shared) {
|
||||
const bottom = -50;
|
||||
const top = -5;
|
||||
@@ -888,7 +842,7 @@ function interpolateScale(table, db) {
|
||||
}
|
||||
|
||||
function interpolateTp(db) {
|
||||
return interpolateScale(TP_PERCENT_SCALE, db);
|
||||
return interpolateScale(TRUE_PEAK_SCALE, db);
|
||||
}
|
||||
|
||||
function deflectionFrac(db) {
|
||||
|
||||
+69
-351
@@ -13,30 +13,19 @@ const METER_SLOTS = 3;
|
||||
const METER_PAD_TOP = 30;
|
||||
const METER_PAD_BOTTOM = 20;
|
||||
const METER_EXTRA_BOTTOM_PAD = 6;
|
||||
const TARGET_POINTS = 1024;
|
||||
const COLOR_STOPS = [
|
||||
{ t: 0.0, color: [0, 0, 50] },
|
||||
{ t: 0.3, color: [0, 120, 180] },
|
||||
{ t: 0.6, color: [0, 205, 120] },
|
||||
{ t: 0.8, color: [210, 220, 0] },
|
||||
{ t: 1.0, color: [255, 120, 0] },
|
||||
];
|
||||
const RING_DBFS = [0, -8, -16, -24, -32, -40];
|
||||
const RING_PPM_DIN = [+5, 0, -10, -20, -30, -40, -50];
|
||||
const PHASE_GAIN_MIN_DB = -35;
|
||||
const PHASE_GAIN_MAX_DB = 35;
|
||||
const PHASE_ALIGN_TARGET = Math.pow(10, -15 / 20);
|
||||
const PHASE_AGC_TARGET_DB = linearToDb(PHASE_ALIGN_TARGET);
|
||||
const PHASE_AGC_BASE_GAIN = 1 / PHASE_ALIGN_TARGET;
|
||||
const PHASE_AGC_ATTACK_S = 0.02;
|
||||
const PHASE_AGC_RELEASE_DB_PER_S = 12;
|
||||
const PHASE_LEVEL_THRESHOLD_DB = -60;
|
||||
const PHASE_LEVEL_THRESHOLD = dbToLinear(PHASE_LEVEL_THRESHOLD_DB);
|
||||
const PHASE_IDLE_DECAY = 0.85;
|
||||
const PHASE_PHASE_SMOOTH_ALPHA = 0.08;
|
||||
const PHASE_RADIUS_SMOOTH_ALPHA = 0.18;
|
||||
const PHASE_BANDPASS_LOW_HZ = 300;
|
||||
const PHASE_BANDPASS_HIGH_HZ = 5000;
|
||||
const PHASE_IDLE_TAU_S = 0.103;
|
||||
const PHASE_SMOOTH_TAU_S = 0.2;
|
||||
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;
|
||||
@@ -48,32 +37,10 @@ const PHASE_SECTORS = [
|
||||
{ startDeg: -180, endDeg: -60, color: 'rgba(255,120,120,0.25)' },
|
||||
];
|
||||
|
||||
// Numerisch stabilere Hilbert-Kernel-Erstellung
|
||||
const HILBERT_KERNEL = buildHilbertKernel(33);
|
||||
const HILBERT_HALF = (HILBERT_KERNEL.length - 1) / 2;
|
||||
|
||||
// Lookup-Tables für häufig verwendete Werte
|
||||
const ANGLE_COS = new Float32Array(360);
|
||||
const ANGLE_SIN = new Float32Array(360);
|
||||
for (let i = 0; i < 360; i++) {
|
||||
const rad = (i * Math.PI) / 180;
|
||||
ANGLE_COS[i] = Math.cos(rad);
|
||||
ANGLE_SIN[i] = Math.sin(rad);
|
||||
}
|
||||
|
||||
export function init() {
|
||||
return {
|
||||
traceBuffer: new Float32Array(0),
|
||||
ampBuffer: new Float32Array(0),
|
||||
filteredL: new Float32Array(0),
|
||||
filteredR: new Float32Array(0),
|
||||
phaseAngleBuffer: new Float32Array(0),
|
||||
phaseAmplitudeBuffer: new Float32Array(0),
|
||||
phaseWeightBuffer: new Float32Array(0),
|
||||
phaseAnalysisSeq: -1,
|
||||
phaseAnalysisLength: 0,
|
||||
phaseAnalysisSampleRate: 0,
|
||||
phaseAnalysisCount: 0,
|
||||
phaseDisplaySeq: -1,
|
||||
phaseDisplayLastTs: 0,
|
||||
phaseConfidence: 0,
|
||||
currentPhase: null,
|
||||
currentRadius: 0,
|
||||
@@ -82,9 +49,7 @@ export function init() {
|
||||
phaseAgcEnv: 1e-3,
|
||||
phaseAgcGainDb: 0,
|
||||
phaseAgcLastTs: 0,
|
||||
bandpass: createBandpassState(),
|
||||
bufferGrowthCount: 0,
|
||||
maxBufferSize: 0,
|
||||
phaseAgcSeq: -1,
|
||||
phaseTrail: [],
|
||||
staticLayerCanvas: null,
|
||||
staticLayerKey: '',
|
||||
@@ -107,13 +72,13 @@ export async function render(env, state) {
|
||||
const now = getNow();
|
||||
|
||||
drawStaticLayer(g, state, rect, layout, CONFIG, slots.length);
|
||||
const xyData = extractXYData(audio);
|
||||
if (xyData.ready) {
|
||||
const gainCtrl = resolvePhaseGain(state, xyData, CONFIG);
|
||||
const trace = buildWheelTrace(state, xyData, layout.wheel, gainCtrl.gain, CONFIG, audio);
|
||||
renderWheel(g, trace, layout.wheel, state, CONFIG, now);
|
||||
const phaseData = extractPhaseData(audio);
|
||||
if (phaseData.ready) {
|
||||
const gainCtrl = resolvePhaseGain(state, phaseData, CONFIG);
|
||||
const updated = updatePhasePointer(state, phaseData, gainCtrl.gain, CONFIG, audio, now);
|
||||
renderWheel(g, updated, layout.wheel, state, CONFIG, now);
|
||||
} else {
|
||||
decayPhasePointer(state);
|
||||
updatePhasePointer(state, phaseData, 1, CONFIG, audio, now);
|
||||
trimPhaseTrail(state, CONFIG, now);
|
||||
drawPhaseTrail(g, layout.wheel, state, CONFIG, now);
|
||||
drawPhasePointer(g, layout.wheel, state);
|
||||
@@ -327,77 +292,54 @@ function radToDeg(rad) {
|
||||
return (rad * 180) / Math.PI;
|
||||
}
|
||||
|
||||
function extractXYData(audio) {
|
||||
const xyL = audio?.xyL;
|
||||
const xyR = audio?.xyR;
|
||||
const isVec = (v) => v && (Array.isArray(v) || ArrayBuffer.isView(v));
|
||||
const ready = !!(audio?.alive && isVec(xyL) && isVec(xyR) && xyL.length && xyR.length);
|
||||
function extractPhaseData(audio) {
|
||||
const angle = Number(audio?.phaseAngleRad);
|
||||
const level = Number(audio?.phaseLevel);
|
||||
const peak = Number(audio?.phasePeak);
|
||||
return {
|
||||
ready,
|
||||
xyL,
|
||||
xyR,
|
||||
length: ready ? Math.min(xyL.length, xyR.length) : 0,
|
||||
sampleRate: audio?.sampleRate || 48000,
|
||||
seq: Number(audio?.xySeq) || 0,
|
||||
ready: !!audio?.alive,
|
||||
angle: Number.isFinite(angle) ? angle : null,
|
||||
coherence: clamp01(Number(audio?.phaseCoherence) || 0),
|
||||
level: Number.isFinite(level) ? Math.max(0, level) : 0,
|
||||
peak: Number.isFinite(peak) ? Math.max(0, peak) : 0,
|
||||
seq: Number(audio?.phaseSeq) || 0,
|
||||
};
|
||||
}
|
||||
|
||||
function buildWheelTrace(state, xyData, wheel, gain = 1, CONFIG, audio) {
|
||||
if (!xyData.ready || !xyData.length) {
|
||||
decayPhasePointer(state);
|
||||
return null;
|
||||
}
|
||||
const analysis = preparePhaseAnalysis(state, xyData);
|
||||
function updatePhasePointer(state, phaseData, gain = 1, CONFIG, audio, nowMs) {
|
||||
const seq = Number(phaseData?.seq) || 0;
|
||||
if (seq > 0 && state.phaseDisplaySeq === seq) return false;
|
||||
const now = Number.isFinite(nowMs) ? nowMs : getNow();
|
||||
const previousTs = Number.isFinite(state.phaseDisplayLastTs) && state.phaseDisplayLastTs > 0
|
||||
? state.phaseDisplayLastTs
|
||||
: now - (1000 / 60);
|
||||
const dt = Math.max(1 / 240, Math.min(0.25, (now - previousTs) / 1000));
|
||||
state.phaseDisplayLastTs = now;
|
||||
state.phaseDisplaySeq = seq;
|
||||
|
||||
const amplitudeMode = getPhaseAmplitudeMode(CONFIG);
|
||||
const ringDbValues = getRingDbValues(CONFIG);
|
||||
const ppmRadiusNorm = amplitudeMode === 'ppm-din'
|
||||
? computePpmDinRadiusNorm(audio, CONFIG, ringDbValues)
|
||||
: 0;
|
||||
const radius = wheel.radius;
|
||||
let ampIdx = 0;
|
||||
let sumRadius = 0;
|
||||
let levelAcc = 0;
|
||||
const gainLinear = Number.isFinite(gain) ? gain : 1;
|
||||
|
||||
for (let i = 0; i < analysis.count; i++) {
|
||||
const angle = analysis.angles[i];
|
||||
const amp = analysis.amplitudes[i];
|
||||
const ampScaled = Math.min(1, amp * gainLinear);
|
||||
const radiusNorm = amplitudeMode === 'ppm-din'
|
||||
? ppmRadiusNorm
|
||||
: linearToRadiusNorm(ampScaled, ringDbValues);
|
||||
ampIdx++;
|
||||
sumRadius += radiusNorm;
|
||||
levelAcc += amp * amp;
|
||||
}
|
||||
|
||||
if (ampIdx > 0) {
|
||||
const invCount = 1 / ampIdx;
|
||||
const phaseSummary = summarizeWeightedPhase(
|
||||
analysis.angles,
|
||||
analysis.weights,
|
||||
analysis.count,
|
||||
);
|
||||
const avgRadius = amplitudeMode === 'ppm-din' ? ppmRadiusNorm : (sumRadius * invCount);
|
||||
const blockLevel = Math.sqrt(levelAcc * invCount);
|
||||
if (blockLevel >= PHASE_LEVEL_THRESHOLD && phaseSummary) {
|
||||
const avgAngle = phaseSummary.angle;
|
||||
const prevPhase = Number.isFinite(state.smoothPhase) ? state.smoothPhase : avgAngle;
|
||||
const prevRadius = Number.isFinite(state.smoothRadius) ? state.smoothRadius : avgRadius;
|
||||
state.currentPhase = avgAngle;
|
||||
state.currentRadius = avgRadius;
|
||||
state.phaseConfidence = phaseSummary.coherence;
|
||||
state.smoothPhase = smoothAngle(prevPhase, avgAngle, PHASE_PHASE_SMOOTH_ALPHA);
|
||||
state.smoothRadius = lerp(prevRadius, avgRadius, PHASE_RADIUS_SMOOTH_ALPHA);
|
||||
} else {
|
||||
state.phaseConfidence = 0;
|
||||
decayPhasePointer(state);
|
||||
}
|
||||
const targetRadius = amplitudeMode === 'ppm-din'
|
||||
? ppmRadiusNorm
|
||||
: linearToRadiusNorm(Math.min(1, phaseData.level * gainLinear), ringDbValues);
|
||||
if (Number.isFinite(phaseData.angle) && phaseData.level >= PHASE_LEVEL_THRESHOLD) {
|
||||
const targetPhase = wrapAngle(phaseData.angle - Math.PI / 2);
|
||||
const prevPhase = Number.isFinite(state.smoothPhase) ? state.smoothPhase : targetPhase;
|
||||
const prevRadius = Number.isFinite(state.smoothRadius) ? state.smoothRadius : targetRadius;
|
||||
state.currentPhase = targetPhase;
|
||||
state.currentRadius = targetRadius;
|
||||
state.phaseConfidence = phaseData.coherence;
|
||||
state.smoothPhase = smoothAngle(prevPhase, targetPhase, smoothingAlpha(dt, PHASE_SMOOTH_TAU_S));
|
||||
state.smoothRadius = lerp(prevRadius, targetRadius, smoothingAlpha(dt, PHASE_RADIUS_TAU_S));
|
||||
} else {
|
||||
state.phaseConfidence = 0;
|
||||
decayPhasePointer(state);
|
||||
decayPhasePointer(state, dt);
|
||||
}
|
||||
return { count: ampIdx, radius };
|
||||
return true;
|
||||
}
|
||||
|
||||
function renderWheel(g, trace, wheel, state, CONFIG, timestamp) {
|
||||
@@ -650,11 +592,6 @@ function trailColorForAngle(angle) {
|
||||
return '#ff7a78';
|
||||
}
|
||||
|
||||
function clamp1(value) {
|
||||
if (!Number.isFinite(value)) return 0;
|
||||
return Math.max(-1, Math.min(1, value));
|
||||
}
|
||||
|
||||
function ringFraction(index, ringCount) {
|
||||
const n = Math.max(1, ringCount | 0);
|
||||
const idx = Math.max(0, Math.min(n - 1, index | 0));
|
||||
@@ -722,205 +659,32 @@ function computePpmDinRadiusNorm(audio, cfg, ringDbValues) {
|
||||
return dbToRadiusNorm(db, ringDbValues);
|
||||
}
|
||||
|
||||
function createBandpassState() {
|
||||
return {
|
||||
sampleRate: 0,
|
||||
hpAlpha: 0,
|
||||
lpAlpha: 0,
|
||||
channels: {
|
||||
L: { hpX: 0, hpY: 0, lpY: 0 },
|
||||
R: { hpX: 0, hpY: 0, lpY: 0 },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function ensureBandpassCoeffs(state, sampleRate) {
|
||||
if (!state.bandpass) state.bandpass = createBandpassState();
|
||||
const sr = Math.max(8000, Math.round(sampleRate) || 48000);
|
||||
if (state.bandpass.sampleRate === sr) return;
|
||||
state.bandpass.sampleRate = sr;
|
||||
state.bandpass.hpAlpha = computeHighpassAlpha(sr, PHASE_BANDPASS_LOW_HZ);
|
||||
state.bandpass.lpAlpha = computeLowpassAlpha(sr, PHASE_BANDPASS_HIGH_HZ);
|
||||
}
|
||||
|
||||
function computeHighpassAlpha(sampleRate, cutoff) {
|
||||
const rc = 1 / (2 * Math.PI * Math.max(1, cutoff));
|
||||
const dt = 1 / Math.max(1, sampleRate);
|
||||
return Math.max(0, Math.min(1, rc / (rc + dt)));
|
||||
}
|
||||
|
||||
function computeLowpassAlpha(sampleRate, cutoff) {
|
||||
const rc = 1 / (2 * Math.PI * Math.max(1, cutoff));
|
||||
const dt = 1 / Math.max(1, sampleRate);
|
||||
return Math.max(0, Math.min(1, dt / (rc + dt)));
|
||||
}
|
||||
|
||||
function ensureFilteredBuffers(state, length) {
|
||||
const neededLength = Math.ceil(length * 1.1); // 10% Puffer für Stabilität
|
||||
if (!state.filteredL || state.filteredL.length < neededLength) {
|
||||
state.filteredL = new Float32Array(neededLength);
|
||||
}
|
||||
if (!state.filteredR || state.filteredR.length < neededLength) {
|
||||
state.filteredR = new Float32Array(neededLength);
|
||||
}
|
||||
return { L: state.filteredL, R: state.filteredR };
|
||||
}
|
||||
|
||||
function applyBandpassSample(sample, channelState, bandpassState) {
|
||||
const hpAlpha = bandpassState.hpAlpha;
|
||||
const lpAlpha = bandpassState.lpAlpha;
|
||||
|
||||
if (!Number.isFinite(sample)) sample = 0;
|
||||
|
||||
const hpY = hpAlpha * (channelState.hpY + sample - channelState.hpX);
|
||||
channelState.hpY = Number.isFinite(hpY) ? hpY : 0;
|
||||
channelState.hpX = sample;
|
||||
|
||||
const lpY = lpAlpha * hpY + (1 - lpAlpha) * channelState.lpY;
|
||||
channelState.lpY = Number.isFinite(lpY) ? lpY : 0;
|
||||
|
||||
return lpY;
|
||||
}
|
||||
|
||||
function preparePhaseFilteredBuffers(state, xyData) {
|
||||
ensureBandpassCoeffs(state, xyData.sampleRate || 48000);
|
||||
const filtered = ensureFilteredBuffers(state, xyData.length);
|
||||
|
||||
// Reset channel states if they contain NaN/Infinity
|
||||
if (!Number.isFinite(state.bandpass.channels.L.hpY)) {
|
||||
state.bandpass.channels.L = { hpX: 0, hpY: 0, lpY: 0 };
|
||||
}
|
||||
if (!Number.isFinite(state.bandpass.channels.R.hpY)) {
|
||||
state.bandpass.channels.R = { hpX: 0, hpY: 0, lpY: 0 };
|
||||
}
|
||||
|
||||
for (let i = 0; i < xyData.length; i++) {
|
||||
filtered.L[i] = applyBandpassSample(
|
||||
xyData.xyL[i],
|
||||
state.bandpass.channels.L,
|
||||
state.bandpass
|
||||
);
|
||||
filtered.R[i] = applyBandpassSample(
|
||||
xyData.xyR[i],
|
||||
state.bandpass.channels.R,
|
||||
state.bandpass
|
||||
);
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
function ensurePhaseAnalysisBuffers(state, length) {
|
||||
if (!state.phaseAngleBuffer || state.phaseAngleBuffer.length < length) {
|
||||
state.phaseAngleBuffer = new Float32Array(length);
|
||||
}
|
||||
if (!state.phaseAmplitudeBuffer || state.phaseAmplitudeBuffer.length < length) {
|
||||
state.phaseAmplitudeBuffer = new Float32Array(length);
|
||||
}
|
||||
if (!state.phaseWeightBuffer || state.phaseWeightBuffer.length < length) {
|
||||
state.phaseWeightBuffer = new Float32Array(length);
|
||||
}
|
||||
}
|
||||
|
||||
export function summarizeWeightedPhase(angles, weights, count) {
|
||||
const limit = Math.max(0, Math.min(
|
||||
Number.isFinite(count) ? Math.floor(count) : 0,
|
||||
angles?.length || 0,
|
||||
weights?.length || 0,
|
||||
));
|
||||
let sumSin = 0;
|
||||
let sumCos = 0;
|
||||
let sumWeight = 0;
|
||||
for (let i = 0; i < limit; i++) {
|
||||
const angle = Number(angles[i]);
|
||||
const weight = Number(weights[i]);
|
||||
if (!Number.isFinite(angle) || !Number.isFinite(weight) || weight <= 0) continue;
|
||||
sumSin += Math.sin(angle) * weight;
|
||||
sumCos += Math.cos(angle) * weight;
|
||||
sumWeight += weight;
|
||||
}
|
||||
if (!(sumWeight > 1e-12)) return null;
|
||||
const resultant = Math.hypot(sumSin, sumCos);
|
||||
return {
|
||||
angle: Math.atan2(sumSin, sumCos),
|
||||
coherence: Math.max(0, Math.min(1, resultant / sumWeight)),
|
||||
weight: sumWeight,
|
||||
};
|
||||
}
|
||||
|
||||
function preparePhaseAnalysis(state, xyData) {
|
||||
const sampleRate = Math.max(1, Math.round(xyData.sampleRate) || 48000);
|
||||
const seq = Number(xyData.seq) || 0;
|
||||
const canReuse = seq > 0
|
||||
&& state.phaseAnalysisSeq === seq
|
||||
&& state.phaseAnalysisLength === xyData.length
|
||||
&& state.phaseAnalysisSampleRate === sampleRate;
|
||||
if (canReuse) {
|
||||
return {
|
||||
angles: state.phaseAngleBuffer,
|
||||
amplitudes: state.phaseAmplitudeBuffer,
|
||||
weights: state.phaseWeightBuffer,
|
||||
count: state.phaseAnalysisCount,
|
||||
};
|
||||
}
|
||||
|
||||
const filtered = preparePhaseFilteredBuffers(state, xyData);
|
||||
const targetPoints = Math.min(TARGET_POINTS, xyData.length);
|
||||
const step = Math.max(1, Math.floor(xyData.length / targetPoints));
|
||||
const sampleCount = Math.ceil(xyData.length / step);
|
||||
ensurePhaseAnalysisBuffers(state, sampleCount);
|
||||
let count = 0;
|
||||
for (let i = 0; i < xyData.length; i += step) {
|
||||
const lRe = clamp1(filtered.L[i]);
|
||||
const rRe = clamp1(filtered.R[i]);
|
||||
const lIm = hilbertAt(filtered.L, i);
|
||||
const rIm = hilbertAt(filtered.R, i);
|
||||
const phaseL = Math.atan2(lIm, lRe);
|
||||
const phaseR = Math.atan2(rIm, rRe);
|
||||
let phaseDiff = phaseL - phaseR;
|
||||
if (!Number.isFinite(phaseDiff)) continue;
|
||||
phaseDiff = wrapAngle(phaseDiff);
|
||||
const magL = Math.min(1, Math.hypot(lRe, lIm));
|
||||
const magR = Math.min(1, Math.hypot(rRe, rIm));
|
||||
state.phaseAngleBuffer[count] = phaseDiff - Math.PI / 2;
|
||||
state.phaseAmplitudeBuffer[count] = Math.min(1, 0.5 * (magL + magR));
|
||||
// The phase of a strong component must contribute more than the phase of
|
||||
// a quiet or one-sided component. magL * magR is the magnitude of the
|
||||
// complex L/R cross power for this analytic sample.
|
||||
state.phaseWeightBuffer[count] = magL * magR;
|
||||
count++;
|
||||
}
|
||||
state.phaseAnalysisSeq = seq;
|
||||
state.phaseAnalysisLength = xyData.length;
|
||||
state.phaseAnalysisSampleRate = sampleRate;
|
||||
state.phaseAnalysisCount = count;
|
||||
return {
|
||||
angles: state.phaseAngleBuffer,
|
||||
amplitudes: state.phaseAmplitudeBuffer,
|
||||
weights: state.phaseWeightBuffer,
|
||||
count,
|
||||
};
|
||||
}
|
||||
|
||||
function decayPhasePointer(state) {
|
||||
function decayPhasePointer(state, dt = 1 / 60) {
|
||||
const prevPhase = Number.isFinite(state.currentPhase) ? state.currentPhase : 0;
|
||||
const prevRadius = Number.isFinite(state.currentRadius) ? state.currentRadius : 0;
|
||||
const decayedRadius = prevRadius * PHASE_IDLE_DECAY;
|
||||
const decayedRadius = prevRadius * Math.exp(-Math.max(0, dt) / PHASE_IDLE_TAU_S);
|
||||
state.currentPhase = prevPhase;
|
||||
state.currentRadius = decayedRadius;
|
||||
const prevSmoothPhase = Number.isFinite(state.smoothPhase) ? state.smoothPhase : prevPhase;
|
||||
const prevSmoothRadius = Number.isFinite(state.smoothRadius) ? state.smoothRadius : decayedRadius;
|
||||
state.smoothPhase = smoothAngle(prevSmoothPhase, prevPhase, PHASE_PHASE_SMOOTH_ALPHA * 0.5);
|
||||
state.smoothRadius = lerp(prevSmoothRadius, decayedRadius, PHASE_RADIUS_SMOOTH_ALPHA);
|
||||
state.smoothPhase = prevSmoothPhase;
|
||||
state.smoothRadius = lerp(prevSmoothRadius, decayedRadius, smoothingAlpha(dt, PHASE_RADIUS_TAU_S));
|
||||
}
|
||||
|
||||
function resolvePhaseGain(state, xyData, CONFIG) {
|
||||
function resolvePhaseGain(state, phaseData, CONFIG) {
|
||||
const gainDb = clampPhaseGain(CONFIG?.PHASE_DISPLAY_GAIN_DB ?? 0);
|
||||
const allowAgc = CONFIG?.PHASE_AGC_ENABLED && getPhaseAmplitudeMode(CONFIG) !== 'ppm-din';
|
||||
if (allowAgc && xyData?.ready) {
|
||||
const auto = computePhaseAgcGain(state, xyData);
|
||||
return { gainDb: auto.gainDb, gain: auto.gain * PHASE_AGC_BASE_GAIN };
|
||||
if (allowAgc && phaseData?.ready) {
|
||||
const seq = Number(phaseData.seq) || 0;
|
||||
if (seq > 0 && state.phaseAgcSeq === seq) {
|
||||
return { gainDb: state.phaseAgcGainDb, gain: dbToLinear(state.phaseAgcGainDb) };
|
||||
}
|
||||
const auto = computePhaseAgcGain(state, phaseData);
|
||||
state.phaseAgcSeq = seq;
|
||||
return { gainDb: auto.gainDb, gain: auto.gain };
|
||||
}
|
||||
state.phaseAgcGainDb = gainDb;
|
||||
state.phaseAgcSeq = Number(phaseData?.seq) || 0;
|
||||
return { gainDb, gain: dbToLinear(gainDb) };
|
||||
}
|
||||
|
||||
@@ -949,51 +713,22 @@ function lerp(a, b, t) {
|
||||
return a + (b - a) * clampedT;
|
||||
}
|
||||
|
||||
function hilbertAt(buffer, idx) {
|
||||
if (!buffer || idx < 0 || idx >= buffer.length) return 0;
|
||||
|
||||
let acc = 0;
|
||||
for (let k = 0; k < HILBERT_KERNEL.length; k++) {
|
||||
const src = idx + k - HILBERT_HALF;
|
||||
if (src < 0 || src >= buffer.length) continue;
|
||||
const sample = buffer[src];
|
||||
const kernel = HILBERT_KERNEL[k];
|
||||
if (Number.isFinite(sample) && Number.isFinite(kernel)) {
|
||||
acc += sample * kernel;
|
||||
}
|
||||
}
|
||||
return Number.isFinite(acc) ? acc : 0;
|
||||
export function smoothingAlpha(dt, tau) {
|
||||
const seconds = Number.isFinite(dt) ? Math.max(0, dt) : 0;
|
||||
const timeConstant = Number.isFinite(tau) ? Math.max(1e-6, tau) : 1e-6;
|
||||
return 1 - Math.exp(-seconds / timeConstant);
|
||||
}
|
||||
|
||||
function buildHilbertKernel(size = 33) {
|
||||
const taps = size % 2 === 0 ? size + 1 : size;
|
||||
const mid = (taps - 1) / 2;
|
||||
const kernel = new Float32Array(taps);
|
||||
|
||||
for (let n = 0; n < taps; n++) {
|
||||
const k = n - mid;
|
||||
|
||||
// Verbesserte numerische Stabilität + even k = 0 wie idealer Hilbert-Kernel
|
||||
if (Math.abs(k) < 1e-10 || k % 2 === 0) {
|
||||
kernel[n] = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
const window = 0.54 - 0.46 * Math.cos((2 * Math.PI * n) / Math.max(1, taps - 1));
|
||||
const value = (2 / (Math.PI * k)) * window;
|
||||
|
||||
// Sicherstellen, dass der Wert finite ist
|
||||
kernel[n] = Number.isFinite(value) ? value : 0;
|
||||
}
|
||||
return kernel;
|
||||
function clamp01(value) {
|
||||
return Number.isFinite(value) ? Math.max(0, Math.min(1, value)) : 0;
|
||||
}
|
||||
|
||||
function computePhaseAgcGain(state, xyData) {
|
||||
function computePhaseAgcGain(state, phaseData) {
|
||||
const now = getNow();
|
||||
const dt = state.phaseAgcLastTs ? Math.max(0, (now - state.phaseAgcLastTs) / 1000) : 0;
|
||||
state.phaseAgcLastTs = now;
|
||||
|
||||
const peak = measurePhasePeak(xyData);
|
||||
const peak = Math.max(1e-8, Number(phaseData?.peak) || 0);
|
||||
let env = Number.isFinite(state.phaseAgcEnv) && state.phaseAgcEnv > 0 ? state.phaseAgcEnv : 1e-3;
|
||||
|
||||
if (peak >= env) {
|
||||
@@ -1017,23 +752,6 @@ function computePhaseAgcGain(state, xyData) {
|
||||
return { gainDb, gain: dbToLinear(gainDb) };
|
||||
}
|
||||
|
||||
function measurePhasePeak(xyData) {
|
||||
if (!xyData || !xyData.ready) return 1e-4; // Verbesserter Default-Wert
|
||||
|
||||
let peak = 1e-8; // Höhere Präzision
|
||||
const len = Math.min(xyData.length, 1000); // Begrenzung für Performance
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
const sample = Math.max(
|
||||
Math.abs(xyData.xyL[i] || 0),
|
||||
Math.abs(xyData.xyR[i] || 0)
|
||||
);
|
||||
if (sample > peak) peak = sample;
|
||||
}
|
||||
|
||||
return Math.max(1e-8, peak); // Sicherstellen, dass nicht 0 zurückgegeben wird
|
||||
}
|
||||
|
||||
function clampPhaseGain(db) {
|
||||
let val = Number(db);
|
||||
if (!Number.isFinite(val)) val = 0;
|
||||
|
||||
@@ -79,7 +79,7 @@ export async function render(env, state) {
|
||||
const useNativeFftEngine = !useIirEngine && rtaData && rtaData.engine === 'fft';
|
||||
const nativeRtaPacket = (useIirEngine || useNativeFftEngine) ? rtaData : null;
|
||||
const displayRtaPacket = (nativeRtaPacket && CONFIG.RTA_BAR_LAYOUT === 'rtw')
|
||||
? selectLocalRtwPacket(nativeRtaPacket, nativeRtaPacket.bpo || '1_3')
|
||||
? selectLocalRtwPacket(nativeRtaPacket, CONFIG.RTA_BPO_MODE || nativeRtaPacket.bpo || '1_3')
|
||||
: nativeRtaPacket;
|
||||
|
||||
const topInset = Number.isFinite(env?.topInset) ? Number(env.topInset) : DEFAULT_TOP_INSET;
|
||||
@@ -824,11 +824,13 @@ function buildFixedRtwBands(bpoMode, freqBounds, nyq, overrideCenters) {
|
||||
return bands;
|
||||
}
|
||||
|
||||
function selectLocalRtwPacket(packet, bpoMode) {
|
||||
export function selectLocalRtwPacket(packet, bpoMode) {
|
||||
if (!packet || !isVectorLike(packet.centers) || !packet.centers.length) return packet;
|
||||
const desiredCenters = getRtwCenters(bpoMode);
|
||||
if (!desiredCenters.length) return packet;
|
||||
if (packet.centers.length === desiredCenters.length) return packet;
|
||||
if (packet.centers.length === desiredCenters.length) {
|
||||
return packet.bpo === bpoMode ? packet : { ...packet, bpo: bpoMode };
|
||||
}
|
||||
|
||||
const keyFor = (value) => Number(value).toFixed(1);
|
||||
const indexByCenter = new Map();
|
||||
@@ -873,6 +875,7 @@ function selectLocalRtwPacket(packet, bpoMode) {
|
||||
|
||||
return {
|
||||
...packet,
|
||||
bpo: bpoMode,
|
||||
centers: selectedCenters,
|
||||
bands_avg: pickVector(packet.bands_avg || packet.bands),
|
||||
bands_peak: pickVector(packet.bands_peak),
|
||||
|
||||
Reference in New Issue
Block a user