1253 lines
57 KiB
JavaScript
1253 lines
57 KiB
JavaScript
// core/config.js — zentrale CONFIG + Presets + Persistenz
|
||
|
||
function isLoopbackHost(host) {
|
||
const h = String(host || '').trim().toLowerCase();
|
||
return h === 'localhost' || h === '127.0.0.1' || h === '::1' || h === '[::1]';
|
||
}
|
||
|
||
function normalizePhoenixBaseUrlForCurrentClient(rawUrl) {
|
||
const fallback = resolvePhoenixDefaultBaseUrl();
|
||
const input = String(rawUrl || '').trim();
|
||
if (!input) return fallback;
|
||
try {
|
||
const currentHost = String(globalThis?.location?.hostname || '').trim();
|
||
const parsed = new URL(input, fallback);
|
||
if (!currentHost) return parsed.toString();
|
||
if (!isLoopbackHost(currentHost)) {
|
||
const parsedHost = String(parsed.hostname || '').trim();
|
||
if (isLoopbackHost(parsedHost)) {
|
||
parsed.hostname = currentHost;
|
||
}
|
||
}
|
||
return parsed.toString();
|
||
} catch (_) {
|
||
return fallback;
|
||
}
|
||
}
|
||
|
||
function resolvePhoenixDefaultBaseUrl() {
|
||
try {
|
||
const host = String(globalThis?.location?.hostname || '').trim();
|
||
if (host) return `http://${host}:8789`;
|
||
} catch (_) {}
|
||
return 'http://127.0.0.1:8789';
|
||
}
|
||
|
||
const DEFAULT_PHOENIX_BASE_URL = resolvePhoenixDefaultBaseUrl();
|
||
const SOFTWARE_PRESET_ENDPOINT = '/api/v1/frontend-presets';
|
||
const LAYOUT_PRESET_ENDPOINT = '/api/v1/frontend-layouts';
|
||
|
||
// Define CONFIG first, then export it
|
||
const CONFIG = {
|
||
FFT_SIZE: 8192,
|
||
PHOENIX_BASE_URL: DEFAULT_PHOENIX_BASE_URL,
|
||
DBFS_TOP: 9,
|
||
DBFS_BOTTOM: -63,
|
||
AXIS_GUTTER_LEFT: 14,
|
||
Y_TICKS: [],
|
||
AL_MARKERS_ENABLED: false,
|
||
REALTIME_RENDER_STYLE: 'bars',
|
||
REALTIME_BAR_HOLD_MS: 800,
|
||
REALTIME_BAR_DECAY_DB_PER_S: 20,
|
||
RTA_ENGINE: 'iir', // 'fft' | 'iir'
|
||
RTA_FREQ_RANGE: 'norm', // 'norm' | 'lf'
|
||
RTA_BPO_MODE: '1_6',
|
||
RTA_WEIGHTING: 'z', // 'z' | 'a' | 'c'
|
||
RTA_DISPLAY_GAIN_FFT_DB: 20,
|
||
RTA_DISPLAY_GAIN_IIR_DB: 20,
|
||
RTA_BAR_LAYOUT: 'rtw', // 'iec' | 'rtw'
|
||
RTA_BAR_BASE_COLOR: '#ffe066',
|
||
HEADER_TEXT_COLOR: '#ffe066',
|
||
RTA_INTEGRATION: 'fast', // 'impulse' | 'fast' | 'slow' | 'peak'
|
||
RTA_BALLISTICS_MODE: 'average', // 'average' | 'peak' | 'both'
|
||
RTA_PEAK_HOLD_MODE: 'auto', // 'off' | 'auto' | 'manual'
|
||
RTA_PEAK_HOLD_SEC: 2,
|
||
RTA_PEAK_DECAY_DB_PER_S: 20,
|
||
RTA_DISPLAY_HOLD_SEC: 0,
|
||
RTA_IIR_ORDER: 4,
|
||
RTA_IIR_TAU_FAST: 0.12,
|
||
RTA_IIR_TAU_SLOW: 1.0,
|
||
SPECTRO_GAMMA: 0.4,
|
||
SPECTRO_SCROLL_MODE: 1, // 0.5=Langsam, 1=Normal, 2=Licht, 4=Lächerlich, 6=Wahnsinnig
|
||
PEAK_HISTORY_SCROLL_MODE: 1, // 0.5=Langsam, 1=Normal, 2=Schnell, 4=Turbo, 6=Max
|
||
PEAK_HISTORY_FILL_ENABLED: false,
|
||
PEAK_HISTORY_FILL_INVERT: false,
|
||
PEAK_HISTORY_SLOT: 'ppm-din',
|
||
SPECTRO_USE_WORKER: true, // Falls true: Offscreen-Worker; false: Fallback im Main-Thread
|
||
SPECTRO_DEBUG: false,
|
||
WAVEFORM_WINDOW_SEC: 1,
|
||
WAVEFORM_MODE: 'stacked',
|
||
WAVEFORM_COLOR_LEFT: '#00e7ff',
|
||
WAVEFORM_COLOR_RIGHT: '#ff6b81',
|
||
WAVEFORM_COLOR_DIFF: '#00e7ff',
|
||
INPUT_OFFSET_DB_L: -5.0,
|
||
INPUT_OFFSET_DB_R: -5.0,
|
||
INPUT_OFFSET_GAIN_L: Math.pow(10, -5.0 / 20),
|
||
INPUT_OFFSET_GAIN_R: Math.pow(10, -5.0 / 20),
|
||
MONO_INPUT: false,
|
||
LR_FRACTIONAL_DELAY_ENABLED: true,
|
||
LR_FRACTIONAL_DELAY_SAMPLES: 0.996,
|
||
SCREENSAVER_ENABLED: true,
|
||
SCREENSAVER_IDLE_MIN: 5,
|
||
SCREENSAVER_ACTIVITY_DB: -50,
|
||
SCREENSAVER_MODE: 'clock', // 'dvd' | 'starfield' | 'clock' | 'black'
|
||
SCREENSAVER_LED_GLOW: true,
|
||
SCREENSAVER_LED_COLOR: '#ff0000',
|
||
CLOCK_LED_GLOW: true,
|
||
CLOCK_LED_COLOR: '#ff0000',
|
||
CLOCK_STYLE: 'analog', // 'analog' | 'digital' | 'analog-ppm' | 'digital-ppm'
|
||
CLOCK_PPM_LED_MODE: false,
|
||
// Alignment-Profile (EBU default) definieren die digitale Referenz für 0 dBu.
|
||
ALIGNMENT_MODE: 'ebu',
|
||
DIGITAL_REF_DBFS_RMS: -18.0, // 0 dBu ≙ −18 dBFS RMS (EBU)
|
||
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_HEADER_SHOW_VALUE: false,
|
||
TP_OFFSET_DB: 0.0,
|
||
TP_HEADER_SHOW_VALUE: false,
|
||
RMS_OFFSET_DB: 0.0,
|
||
RMS_HEADER_SHOW_VALUE: false,
|
||
VU_RED_START: 0.0,
|
||
TP_RED_START: -1.0,
|
||
RMS_RED_START: -3.0,
|
||
VU_RED_BAR_ONLY: true,
|
||
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)
|
||
VU_COLOR_NORMAL: '#ffe066',
|
||
VU_COLOR_WARN: '#ff3b3b',
|
||
PPM_DIN_COLOR_NORMAL: '#ffe066',
|
||
PPM_DIN_COLOR_WARN: '#ff3b3b',
|
||
PPM_DIN_HEADER_SHOW_VALUE: false,
|
||
PPM_EBU_COLOR_NORMAL: '#ffe066',
|
||
PPM_EBU_COLOR_WARN: '#ff3b3b',
|
||
PPM_EBU_HEADER_SHOW_VALUE: false,
|
||
TP_COLOR_NORMAL: '#ffe066',
|
||
TP_COLOR_WARN: '#ff3b3b',
|
||
HIFI_PEAK_ALIGNMENT: 'ppm_din_minus5',
|
||
PPM_DIN_LOUDNESS_BOXES: true,
|
||
PPM_DIN_LOUDNESS_OFFSET_DB: 0.0,
|
||
// RMS colours: use a green bar for normal RMS to distinguish from PPM/VU.
|
||
RMS_COLOR_NORMAL: '#34d399',
|
||
RMS_COLOR_WARN: '#ff3b3b',
|
||
// RMS meter defaults to dBFS mode. In German Rundfunk entspricht der
|
||
// Alignment‑Level (0 dBu) −18 dBFS RMS【212988610891871†L41-L43】; 0 VU (+4 dBu)
|
||
// liegt 4 dB darüber bei −14 dBFS. Viele Anwender möchten die digitale
|
||
// Anzeige (dBFS) sehen. Setze daher RMS_MODE auf 'dbfs'. Wenn RMS_MODE
|
||
// auf 'dbu' umgeschaltet würde, nutzt die Umrechnung die folgenden
|
||
// Referenzen: 0 dBu entspricht −18 dBFS RMS (RMS_REF_DBFS_FOR_REF_DBU) und
|
||
// RMS_REF_DBU definiert den dBu‑Wert bei diesem Pegel. Wir setzen
|
||
// RMS_REF_DBU auf 0, sodass −18 dBFS → 0 dBu und −14 dBFS → +4 dBu.
|
||
RMS_MODE: 'dbfs',
|
||
RMS_REF_DBFS_FOR_REF_DBU: -18,
|
||
RMS_REF_DBU: 0,
|
||
// EBU (Type IIb) PPM scale settings. The scale runs from −12 dB to
|
||
// +12 dB with the red zone starting at +9 dB. Attack and decay
|
||
// parameters are defined below.
|
||
PPM_EBU_TOP: +12,
|
||
PPM_EBU_BOTTOM: -12,
|
||
PPM_EBU_RED_START: +9,
|
||
// DIN (Type I) PPM scale settings. The DIN scale covers −50 dB to +5 dB
|
||
// and the red zone begins at 0 dB. Attack and decay parameters follow.
|
||
PPM_DIN_TOP: +5,
|
||
PPM_DIN_BOTTOM: -50,
|
||
PPM_DIN_RED_START: 0,
|
||
// DIN PPM attack and decay times: 5 ms integration and 20 dB in 1.7 s
|
||
// (≈11.8 dB/s) return, per IEC 60268‑10 Type I.
|
||
PPM_DIN_ATTACK_MS: 5,
|
||
// 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: 11.8,
|
||
// Hold time for DIN PPM: verlängert auf 1000 ms (1 s), damit der Peak‑Hold
|
||
// auch bei kurzen Transienten deutlich sichtbar bleibt. Gemäß
|
||
// Rundfunkpraxis werden Peak‑Hold‑Striche häufig 1–2 Sekunden gehalten.
|
||
PPM_DIN_HOLD_MS: 1000,
|
||
// Decay rate for the DIN PPM hold. When the held peak decays, it uses
|
||
// the same slope as the main meter (≈11.8 dB/s).
|
||
PPM_DIN_HOLD_DECAY_DB_PER_S: 11.8,
|
||
|
||
// Offsets für die PPM-Meter (DIN/EBU-spezifisch). Die EBU/DIN Offsets
|
||
// ersetzen den früheren globalen PPM_OFFSET.
|
||
PPM_DIN_OFFSET: -8.90,
|
||
PPM_EBU_OFFSET: 0.0,
|
||
PPM_REF_DBFS_PEAK_FOR_0_DBU: -15.0,
|
||
|
||
// Ballistic parameters for the EBU PPM (Type IIb). Attack ~10 ms and
|
||
// decay 24 dB in 2.8 s (≈8.6 dB/s), per IEC 60268‑10 Type II. A short
|
||
// peak-hold (~750 ms) helps visualise transients. Hold decay matches
|
||
// the main decay rate.
|
||
PPM_EBU_ATTACK_MS: 10,
|
||
PPM_EBU_DECAY_DB_PER_S: 8.6,
|
||
PPM_EBU_HOLD_MS: 750,
|
||
PPM_EBU_HOLD_DECAY_DB_PER_S: 8.6,
|
||
CORR_SMOOTH: 0.85,
|
||
CORR_ZERO_ON_SILENCE: false,
|
||
CORR_SILENCE_THRESHOLD_RMS_DBFS: -75,
|
||
XY_POINTS: 1024,
|
||
XY_STYLE: 'lines',
|
||
XY_SILENCE_GATE_ENABLED: true,
|
||
XY_SILENCE_THRESHOLD_RMS_DBFS: -75,
|
||
METER_BAR_THIN: 0.55,
|
||
GONI_METER_GAP: 6,
|
||
GONIO_DISPLAY_GAIN_DB: -5,
|
||
GONIO_MANUAL_GAIN_DB: -5,
|
||
GONIO_AGC_ENABLED: false,
|
||
GONIO_LINE_FADE_MS: 50,
|
||
PHASE_DISPLAY_GAIN_DB: 0,
|
||
PHASE_AGC_ENABLED: false,
|
||
PHASE_TRAIL_ENABLED: true,
|
||
PHASE_AMPLITUDE_MODE: 'ppm-din', // 'bandpass' | 'ppm-din'
|
||
CLASSIC_NEEDLES_SLOT: 'vu',
|
||
PANEL_DIVIDERS_ENABLED: true,
|
||
LUFS_RED_START: -1.0,
|
||
LUFS_YELLOW_START: -2.0,
|
||
LUFS_GREEN_START: -5.0,
|
||
LUFS_COLOR_GREEN: '#34d399',
|
||
LUFS_COLOR_YELLOW: '#ffe066',
|
||
LUFS_COLOR_RED: '#ff3b3b',
|
||
LUFS_COLOR_I_GREEN: '#34d399',
|
||
LUFS_COLOR_I_YELLOW: '#ffe066',
|
||
LUFS_COLOR_I_RED: '#ff3b3b',
|
||
LUFS_COLOR_I: '#34d399',
|
||
LUFS_COLOR_M_GREEN: '#34d399',
|
||
LUFS_COLOR_M_YELLOW: '#ffe066',
|
||
LUFS_COLOR_M_RED: '#ff3b3b',
|
||
LUFS_COLOR_M: '#ffe066',
|
||
LUFS_COLOR_S_GREEN: '#34d399',
|
||
LUFS_COLOR_S_YELLOW: '#ffe066',
|
||
LUFS_COLOR_S_RED: '#ff3b3b',
|
||
LUFS_COLOR_S: '#ff3b3b',
|
||
LUFS_SCALE_LABEL_COLOR: '#8fd3d4',
|
||
// LUFS Integrated: rollierendes Zeitfenster (Minuten).
|
||
// Hinweis: Erhöhen kann bereits verworfene Historie nicht zurückholen.
|
||
LUFS_I_WINDOW_MIN: 4,
|
||
// LUFS Integrated: normgerecht über gesamte Laufzeit (seit Start/Reset),
|
||
// deaktiviert das rollierende Zeitfenster.
|
||
LUFS_I_NORM_ENABLED: false,
|
||
// Stoppuhr: Anzeige-Stil im Slot-Meter.
|
||
// 'mono' = normale Canvas-Schrift, 'seven' = 7-Segment-Rendering.
|
||
STOPWATCH_DISPLAY_STYLE: 'mono',
|
||
STOPWATCH_AUTO_THRESHOLD_DBFS: -70,
|
||
// UI: zuletzt gewählte Ansicht (Fallback, falls localStorage-Flush bei hartem Neustart zu spät ist).
|
||
UI_LAST_STYLE: 'realtime',
|
||
UI_LAST_NON_OPTION_STYLE: 'realtime',
|
||
RECORD_OUTPUT_FORMAT: 'wav', // 'wav' | 'mp3' | 'webm'
|
||
RECORD_MP3_BITRATE_KBPS: 192,
|
||
RECORD_TARGET: 'download', // 'download' | 'analyzer' | 'volumio'
|
||
RECORD_PI_TARGET: 'analyzer', // globales Pi-Ziel: 'analyzer' | 'volumio'
|
||
RECORD_AUTO_DOWNLOAD: false,
|
||
RECORD_AUTO_SPLIT_GAP_SEC: 1.5,
|
||
RECORD_AUTO_THRESHOLD_DBFS: -50,
|
||
RECORD_SHOW_RECORDER_AB: true,
|
||
SPLIT_VIEW_LEFT: 'realtime',
|
||
SPLIT_VIEW_RIGHT: 'goniometer-rtw',
|
||
SPLIT_VIEW_TAP_ACTION: 'popup', // 'switch' | 'popup'
|
||
QUAD_VIEW_TL: 'peak-history',
|
||
QUAD_VIEW_TR: 'realtime',
|
||
QUAD_VIEW_BL: 'phase-wheel',
|
||
QUAD_VIEW_BR: 'goniometer-rtw',
|
||
QUAD_VIEW_TAP_ACTION: 'popup', // 'switch' | 'popup'
|
||
QUAD_VIEW_METER_COUNT: 1, // 0..3
|
||
QUAD_VIEW_METER_1: 'ppm-din',
|
||
QUAD_VIEW_METER_1_POS: 'right', // 'left' | 'center' | 'right'
|
||
QUAD_VIEW_METER_2: 'ppm-din',
|
||
QUAD_VIEW_METER_2_POS: 'right',
|
||
QUAD_VIEW_METER_3: 'lufs',
|
||
QUAD_VIEW_METER_3_POS: 'right',
|
||
SPLIT_VIEW_METER_COUNT: 1, // 0..3
|
||
SPLIT_VIEW_METER_1: 'ppm-din',
|
||
SPLIT_VIEW_METER_1_POS: 'center', // 'left' | 'center' | 'right'
|
||
SPLIT_VIEW_METER_2: 'ppm-din',
|
||
SPLIT_VIEW_METER_2_POS: 'right',
|
||
SPLIT_VIEW_METER_3: 'lufs',
|
||
SPLIT_VIEW_METER_3_POS: 'right',
|
||
|
||
// Hold and decay times for VU meters.
|
||
// VU_HOLD_MS: how long (in ms) to hold the VU peak before it starts decaying.
|
||
// VU_DECAY_DB_PER_S: decay speed in dB per second for VU peaks.
|
||
VU_HOLD_MS: 1000,
|
||
VU_DECAY_DB_PER_S: 11.1,
|
||
TP_HOLD_MS: 1000,
|
||
TP_DECAY_DB_PER_S: 20,
|
||
PPM_DIN_TRIM_DB: 0,
|
||
PPM_DIN_MODE: 'al_minus6',
|
||
SOFTWARE_PRESET: 'default',
|
||
};
|
||
const CONFIG_DEFAULTS = JSON.parse(JSON.stringify(CONFIG));
|
||
const SOFTWARE_PRESET_IDS = Object.freeze(['default', 'claus', 'michael']);
|
||
const LAYOUT_PRESET_IDS = Object.freeze(['1', '2', '3', '4']);
|
||
const LAYOUT_PRESET_KEYS = Object.freeze([
|
||
'UI_LAST_STYLE',
|
||
'UI_LAST_NON_OPTION_STYLE',
|
||
'SPLIT_VIEW_LEFT',
|
||
'SPLIT_VIEW_RIGHT',
|
||
'SPLIT_VIEW_TAP_ACTION',
|
||
'SPLIT_VIEW_METER_COUNT',
|
||
'SPLIT_VIEW_METER_1',
|
||
'SPLIT_VIEW_METER_1_POS',
|
||
'SPLIT_VIEW_METER_2',
|
||
'SPLIT_VIEW_METER_2_POS',
|
||
'SPLIT_VIEW_METER_3',
|
||
'SPLIT_VIEW_METER_3_POS',
|
||
'QUAD_VIEW_TL',
|
||
'QUAD_VIEW_TR',
|
||
'QUAD_VIEW_BL',
|
||
'QUAD_VIEW_BR',
|
||
'QUAD_VIEW_TAP_ACTION',
|
||
'QUAD_VIEW_METER_COUNT',
|
||
'QUAD_VIEW_METER_1',
|
||
'QUAD_VIEW_METER_1_POS',
|
||
'QUAD_VIEW_METER_2',
|
||
'QUAD_VIEW_METER_2_POS',
|
||
'QUAD_VIEW_METER_3',
|
||
'QUAD_VIEW_METER_3_POS',
|
||
]);
|
||
|
||
function cloneDefaultConfigForPreset(id) {
|
||
const snapshot = JSON.parse(JSON.stringify(CONFIG_DEFAULTS));
|
||
snapshot.SOFTWARE_PRESET = id;
|
||
return snapshot;
|
||
}
|
||
|
||
const SOFTWARE_PRESETS = Object.freeze({
|
||
default: Object.freeze(cloneDefaultConfigForPreset('default')),
|
||
claus: Object.freeze(cloneDefaultConfigForPreset('claus')),
|
||
michael: Object.freeze(cloneDefaultConfigForPreset('michael')),
|
||
});
|
||
|
||
function resolvePhoenixApiUrl(pathname) {
|
||
const base = normalizePhoenixBaseUrlForCurrentClient(CONFIG.PHOENIX_BASE_URL || DEFAULT_PHOENIX_BASE_URL || '');
|
||
return `${base}${pathname}`;
|
||
}
|
||
|
||
function resolveCurrentClientPhoenixApiUrl(pathname) {
|
||
const host = String(globalThis?.location?.hostname || '').trim();
|
||
if (!host) return resolvePhoenixApiUrl(pathname);
|
||
return `http://${host}:8789${pathname}`;
|
||
}
|
||
|
||
async function fetchJsonWithPhoenixFallback(pathname, init = {}) {
|
||
const primaryUrl = resolvePhoenixApiUrl(pathname);
|
||
const fallbackUrl = resolveCurrentClientPhoenixApiUrl(pathname);
|
||
let lastError = null;
|
||
|
||
const run = async (url) => {
|
||
const res = await fetch(url, { cache: 'no-store', ...init });
|
||
if (!res.ok) throw new Error(`request failed (${res.status})`);
|
||
return await res.json();
|
||
};
|
||
|
||
try {
|
||
return await run(primaryUrl);
|
||
} catch (err) {
|
||
lastError = err;
|
||
if (fallbackUrl !== primaryUrl) {
|
||
try {
|
||
return await run(fallbackUrl);
|
||
} catch (err2) {
|
||
lastError = err2;
|
||
}
|
||
}
|
||
}
|
||
throw lastError || new Error('request failed');
|
||
}
|
||
|
||
function buildSoftwarePresetSnapshot(extra = null) {
|
||
const snapshot = JSON.parse(JSON.stringify(CONFIG));
|
||
delete snapshot.Y_TICKS;
|
||
delete snapshot.INPUT_OFFSET_GAIN_L;
|
||
delete snapshot.INPUT_OFFSET_GAIN_R;
|
||
if (extra && typeof extra === 'object' && !Array.isArray(extra)) {
|
||
snapshot.__PHOENIX_UI_STATE = JSON.parse(JSON.stringify(extra));
|
||
} else {
|
||
delete snapshot.__PHOENIX_UI_STATE;
|
||
}
|
||
return snapshot;
|
||
}
|
||
|
||
function applyConfigSnapshot(snapshot, opts = {}) {
|
||
const presetId = normalizeSoftwarePresetId(opts.presetId || snapshot?.SOFTWARE_PRESET || 'default');
|
||
resetConfigToDefaults();
|
||
let uiState = null;
|
||
if (snapshot && typeof snapshot === 'object' && !Array.isArray(snapshot)) {
|
||
for (const k of Object.keys(snapshot)) {
|
||
if (k === '__PHOENIX_UI_STATE') {
|
||
const extra = snapshot[k];
|
||
if (extra && typeof extra === 'object' && !Array.isArray(extra)) {
|
||
uiState = JSON.parse(JSON.stringify(extra));
|
||
}
|
||
continue;
|
||
}
|
||
if (k in CONFIG) CONFIG[k] = snapshot[k];
|
||
}
|
||
}
|
||
CONFIG.PHOENIX_BASE_URL = normalizePhoenixBaseUrlForCurrentClient(CONFIG.PHOENIX_BASE_URL || CONFIG_DEFAULTS.PHOENIX_BASE_URL || DEFAULT_PHOENIX_BASE_URL);
|
||
CONFIG.SOFTWARE_PRESET = presetId;
|
||
applyAlignmentProfile((CONFIG.ALIGNMENT_MODE === 'ard' || CONFIG.PPM_DIN_MODE === 'al_minus9') ? 'ard' : 'ebu', { persist: false, skipDinStorage: true });
|
||
updateInputOffsetGain({ L: CONFIG.INPUT_OFFSET_DB_L, R: CONFIG.INPUT_OFFSET_DB_R });
|
||
if (opts.persist) saveConfig();
|
||
return { presetId, uiState };
|
||
}
|
||
|
||
async function fetchSoftwarePresetStore() {
|
||
const payload = await fetchJsonWithPhoenixFallback(`${SOFTWARE_PRESET_ENDPOINT}?_=${Date.now()}`);
|
||
const presets = payload?.presets;
|
||
return (presets && typeof presets === 'object' && !Array.isArray(presets)) ? presets : {};
|
||
}
|
||
|
||
function buildLayoutPresetSnapshot(extra = null) {
|
||
const snapshot = {};
|
||
for (const key of LAYOUT_PRESET_KEYS) {
|
||
snapshot[key] = JSON.parse(JSON.stringify(CONFIG[key]));
|
||
}
|
||
if (extra && typeof extra === 'object' && !Array.isArray(extra)) {
|
||
snapshot.__PHOENIX_UI_STATE = JSON.parse(JSON.stringify(extra));
|
||
}
|
||
return snapshot;
|
||
}
|
||
|
||
function applyLayoutSnapshot(snapshot, opts = {}) {
|
||
const layoutId = normalizeLayoutPresetId(opts.layoutId || snapshot?.LAYOUT_PRESET || '');
|
||
let uiState = null;
|
||
if (snapshot && typeof snapshot === 'object' && !Array.isArray(snapshot)) {
|
||
for (const key of Object.keys(snapshot)) {
|
||
if (key === '__PHOENIX_UI_STATE') {
|
||
const extra = snapshot[key];
|
||
if (extra && typeof extra === 'object' && !Array.isArray(extra)) {
|
||
uiState = JSON.parse(JSON.stringify(extra));
|
||
}
|
||
continue;
|
||
}
|
||
if (LAYOUT_PRESET_KEYS.includes(key) && key in CONFIG) {
|
||
CONFIG[key] = snapshot[key];
|
||
}
|
||
}
|
||
}
|
||
if (opts.persist) saveConfig();
|
||
return { layoutId, uiState, snapshot };
|
||
}
|
||
|
||
async function fetchLayoutPresetStore() {
|
||
const payload = await fetchJsonWithPhoenixFallback(`${LAYOUT_PRESET_ENDPOINT}?_=${Date.now()}`);
|
||
const layouts = payload?.layouts;
|
||
return (layouts && typeof layouts === 'object' && !Array.isArray(layouts)) ? layouts : {};
|
||
}
|
||
|
||
async function loadSoftwarePreset(presetId = 'default') {
|
||
const id = normalizeSoftwarePresetId(presetId);
|
||
if (id === 'default') {
|
||
applySoftwarePreset('default', { persist: true });
|
||
return { presetId: 'default', source: 'builtin', snapshot: JSON.parse(JSON.stringify(SOFTWARE_PRESETS.default)), uiState: null };
|
||
}
|
||
const presets = await fetchSoftwarePresetStore();
|
||
const snapshot = presets[id];
|
||
if (snapshot && typeof snapshot === 'object' && !Array.isArray(snapshot)) {
|
||
const applied = applyConfigSnapshot(snapshot, { presetId: id, persist: true });
|
||
return { presetId: id, source: 'stored', snapshot: JSON.parse(JSON.stringify(snapshot)), uiState: applied?.uiState || null };
|
||
}
|
||
applySoftwarePreset(id, { persist: true });
|
||
return { presetId: id, source: 'builtin', snapshot: JSON.parse(JSON.stringify(SOFTWARE_PRESETS[id] || SOFTWARE_PRESETS.default)), uiState: null };
|
||
}
|
||
|
||
async function saveSoftwarePreset(presetId = 'default', snapshot = null) {
|
||
const id = normalizeSoftwarePresetId(presetId);
|
||
if (id === 'default') return 'default';
|
||
CONFIG.SOFTWARE_PRESET = id;
|
||
saveConfig();
|
||
const configSnapshot = (snapshot && typeof snapshot === 'object' && !Array.isArray(snapshot))
|
||
? JSON.parse(JSON.stringify(snapshot))
|
||
: buildSoftwarePresetSnapshot();
|
||
configSnapshot.SOFTWARE_PRESET = id;
|
||
const body = JSON.stringify({ preset_id: id, config: configSnapshot });
|
||
const primaryUrl = resolvePhoenixApiUrl(SOFTWARE_PRESET_ENDPOINT);
|
||
const fallbackUrl = resolveCurrentClientPhoenixApiUrl(SOFTWARE_PRESET_ENDPOINT);
|
||
let res = null;
|
||
let lastError = null;
|
||
try {
|
||
res = await fetch(primaryUrl, {
|
||
method: 'POST',
|
||
headers: { 'content-type': 'application/json' },
|
||
cache: 'no-store',
|
||
body,
|
||
});
|
||
if (!res.ok) throw new Error(`preset save failed (${res.status})`);
|
||
} catch (err) {
|
||
lastError = err;
|
||
if (fallbackUrl !== primaryUrl) {
|
||
res = await fetch(fallbackUrl, {
|
||
method: 'POST',
|
||
headers: { 'content-type': 'application/json' },
|
||
cache: 'no-store',
|
||
body,
|
||
});
|
||
if (!res.ok) throw new Error(`preset save failed (${res.status})`);
|
||
} else {
|
||
throw lastError;
|
||
}
|
||
}
|
||
return id;
|
||
}
|
||
|
||
async function loadLayoutPreset(layoutId = '1') {
|
||
const id = normalizeLayoutPresetId(layoutId);
|
||
if (!id) return null;
|
||
const layouts = await fetchLayoutPresetStore();
|
||
const snapshot = layouts[id];
|
||
if (snapshot && typeof snapshot === 'object' && !Array.isArray(snapshot)) {
|
||
const applied = applyLayoutSnapshot(snapshot, { layoutId: id, persist: true });
|
||
return { layoutId: id, source: 'stored', snapshot: JSON.parse(JSON.stringify(snapshot)), uiState: applied?.uiState || null };
|
||
}
|
||
return null;
|
||
}
|
||
|
||
async function saveLayoutPreset(layoutId = '1', extra = null) {
|
||
const id = normalizeLayoutPresetId(layoutId);
|
||
if (!id) throw new Error('invalid layout id');
|
||
const configSnapshot = buildLayoutPresetSnapshot(extra);
|
||
const body = JSON.stringify({ preset_id: id, config: configSnapshot });
|
||
const primaryUrl = resolvePhoenixApiUrl(LAYOUT_PRESET_ENDPOINT);
|
||
const fallbackUrl = resolveCurrentClientPhoenixApiUrl(LAYOUT_PRESET_ENDPOINT);
|
||
let res = null;
|
||
try {
|
||
res = await fetch(primaryUrl, {
|
||
method: 'POST',
|
||
headers: { 'content-type': 'application/json' },
|
||
cache: 'no-store',
|
||
body,
|
||
});
|
||
if (!res.ok) throw new Error(`layout save failed (${res.status})`);
|
||
} catch (err) {
|
||
if (fallbackUrl !== primaryUrl) {
|
||
res = await fetch(fallbackUrl, {
|
||
method: 'POST',
|
||
headers: { 'content-type': 'application/json' },
|
||
cache: 'no-store',
|
||
body,
|
||
});
|
||
if (!res.ok) throw new Error(`layout save failed (${res.status})`);
|
||
} else {
|
||
throw err;
|
||
}
|
||
}
|
||
return id;
|
||
}
|
||
|
||
async function exportSoftwarePreset(presetId = 'default') {
|
||
const id = normalizeSoftwarePresetId(presetId);
|
||
let snapshot = null;
|
||
if (id === 'default') {
|
||
snapshot = JSON.parse(JSON.stringify(SOFTWARE_PRESETS.default));
|
||
} else {
|
||
const presets = await fetchSoftwarePresetStore();
|
||
snapshot = (presets[id] && typeof presets[id] === 'object' && !Array.isArray(presets[id]))
|
||
? JSON.parse(JSON.stringify(presets[id]))
|
||
: buildSoftwarePresetSnapshot();
|
||
}
|
||
snapshot.SOFTWARE_PRESET = id;
|
||
return { presetId: id, config: snapshot };
|
||
}
|
||
|
||
async function importSoftwarePreset(presetId = 'default', payload = null) {
|
||
const id = normalizeSoftwarePresetId(presetId);
|
||
if (id === 'default') return 'default';
|
||
let snapshot = payload;
|
||
if (snapshot && typeof snapshot === 'object' && !Array.isArray(snapshot) && snapshot.config && typeof snapshot.config === 'object' && !Array.isArray(snapshot.config)) {
|
||
snapshot = snapshot.config;
|
||
}
|
||
if (!snapshot || typeof snapshot !== 'object' || Array.isArray(snapshot)) {
|
||
throw new Error('invalid preset import payload');
|
||
}
|
||
const normalized = JSON.parse(JSON.stringify(snapshot));
|
||
normalized.SOFTWARE_PRESET = id;
|
||
await saveSoftwarePreset(id, normalized);
|
||
return loadSoftwarePreset(id);
|
||
}
|
||
const PHOENIX_GLOBAL_OPTION_KEYS = Object.freeze([
|
||
'FFT_SIZE',
|
||
'RTA_BPO_MODE',
|
||
'INPUT_OFFSET_DB_L',
|
||
'INPUT_OFFSET_DB_R',
|
||
'MONO_INPUT',
|
||
'LR_FRACTIONAL_DELAY_ENABLED',
|
||
'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',
|
||
'LUFS_I_WINDOW_MIN',
|
||
'LUFS_I_NORM_ENABLED',
|
||
'PPM_DIN_LOUDNESS_BOXES',
|
||
'PPM_DIN_LOUDNESS_OFFSET_DB',
|
||
'XY_POINTS',
|
||
'GONIO_DISPLAY_GAIN_DB',
|
||
'RECORD_OUTPUT_FORMAT',
|
||
'RECORD_MP3_BITRATE_KBPS',
|
||
'RECORD_PI_TARGET',
|
||
'RECORD_AUTO_SPLIT_GAP_SEC',
|
||
'RECORD_AUTO_THRESHOLD_DBFS',
|
||
'SCREENSAVER_ENABLED',
|
||
'SCREENSAVER_IDLE_MIN',
|
||
'SCREENSAVER_ACTIVITY_DB',
|
||
'SCREENSAVER_LED_GLOW',
|
||
'SCREENSAVER_LED_COLOR',
|
||
'CLOCK_LED_COLOR',
|
||
'HEADER_TEXT_COLOR',
|
||
'RTA_BAR_BASE_COLOR',
|
||
'PEAK_HISTORY_SCROLL_MODE',
|
||
'PEAK_HISTORY_FILL_ENABLED',
|
||
'PEAK_HISTORY_FILL_INVERT',
|
||
'PEAK_HISTORY_SLOT',
|
||
'PHASE_DISPLAY_GAIN_DB',
|
||
'PHASE_AGC_ENABLED',
|
||
'PHASE_TRAIL_ENABLED',
|
||
'PHASE_AMPLITUDE_MODE',
|
||
'CLASSIC_NEEDLES_SLOT',
|
||
'WAVEFORM_COLOR_LEFT',
|
||
'WAVEFORM_COLOR_RIGHT',
|
||
'WAVEFORM_COLOR_DIFF',
|
||
'VU_COLOR_NORMAL',
|
||
'VU_COLOR_WARN',
|
||
'PPM_DIN_COLOR_NORMAL',
|
||
'PPM_DIN_COLOR_WARN',
|
||
'PPM_EBU_COLOR_NORMAL',
|
||
'PPM_EBU_COLOR_WARN',
|
||
'TP_COLOR_NORMAL',
|
||
'TP_COLOR_WARN',
|
||
'RMS_COLOR_NORMAL',
|
||
'RMS_COLOR_WARN',
|
||
'LUFS_COLOR_I',
|
||
'LUFS_COLOR_M',
|
||
'LUFS_COLOR_S',
|
||
'LUFS_SCALE_LABEL_COLOR',
|
||
]);
|
||
|
||
const ALIGNMENT_PROFILES = {
|
||
ebu: {
|
||
id: 'ebu',
|
||
dinMode: 'al_minus6',
|
||
digitalRefRms: -18,
|
||
},
|
||
ard: {
|
||
id: 'ard',
|
||
dinMode: 'al_minus9',
|
||
digitalRefRms: -15,
|
||
},
|
||
};
|
||
|
||
const BROADCAST_STANDARDS = {
|
||
'EBU R128': {
|
||
VU_DBFS_REF: -18.0, PPM_EBU_TOP: +12, PPM_EBU_RED_START: +9, TP_RED_START: -1.0,
|
||
LUFS_RED_START: -1.0, LUFS_YELLOW_START: -2.0, LUFS_GREEN_START: -5.0,
|
||
VU_RED_START: 0,
|
||
},
|
||
'ATSC A/85': {
|
||
VU_DBFS_REF: -20.0, PPM_EBU_TOP: +12, PPM_EBU_RED_START: +9, TP_RED_START: -2.0,
|
||
LUFS_RED_START: -2.0, LUFS_YELLOW_START: -4.0, LUFS_GREEN_START: -8.0,
|
||
VU_RED_START: +2,
|
||
},
|
||
'BBC': {
|
||
VU_DBFS_REF: -18.0, PPM_EBU_TOP: +12, PPM_EBU_RED_START: +9, TP_RED_START: -1.0,
|
||
LUFS_RED_START: -1.0, LUFS_YELLOW_START: -3.0, LUFS_GREEN_START: -6.0,
|
||
VU_RED_START: +1,
|
||
},
|
||
'DIN': {
|
||
VU_DBFS_REF: -15.0, PPM_EBU_TOP: +12, PPM_EBU_RED_START: +9, TP_RED_START: 0.0,
|
||
LUFS_RED_START: 0.0, LUFS_YELLOW_START: -2.0, LUFS_GREEN_START: -4.0,
|
||
VU_RED_START: +3,
|
||
}
|
||
|
||
};
|
||
|
||
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 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';
|
||
let configLoadedOnce = false;
|
||
|
||
function normalizeSoftwarePresetId(value) {
|
||
const id = String(value || '').trim().toLowerCase();
|
||
return SOFTWARE_PRESET_IDS.includes(id) ? id : 'default';
|
||
}
|
||
|
||
function normalizeLayoutPresetId(value) {
|
||
const id = String(value || '').trim().toLowerCase();
|
||
if (LAYOUT_PRESET_IDS.includes(id)) return id;
|
||
if (id === 'layout1' || id === 'slot1' || id === 'a') return '1';
|
||
if (id === 'layout2' || id === 'slot2' || id === 'b') return '2';
|
||
if (id === 'layout3' || id === 'slot3' || id === 'c') return '3';
|
||
if (id === 'layout4' || id === 'slot4' || id === 'd') return '4';
|
||
return '';
|
||
}
|
||
|
||
function applySoftwarePreset(presetId = 'default', opts = {}) {
|
||
const id = normalizeSoftwarePresetId(presetId);
|
||
const snapshot = SOFTWARE_PRESETS[id] || SOFTWARE_PRESETS.default;
|
||
Object.assign(CONFIG, JSON.parse(JSON.stringify(snapshot)));
|
||
CONFIG.PHOENIX_BASE_URL = normalizePhoenixBaseUrlForCurrentClient(CONFIG.PHOENIX_BASE_URL || CONFIG_DEFAULTS.PHOENIX_BASE_URL || DEFAULT_PHOENIX_BASE_URL);
|
||
CONFIG.SOFTWARE_PRESET = id;
|
||
applyAlignmentProfile((CONFIG.ALIGNMENT_MODE === 'ard' || CONFIG.PPM_DIN_MODE === 'al_minus9') ? 'ard' : 'ebu', { persist: false, skipDinStorage: true });
|
||
updateInputOffsetGain({ L: CONFIG.INPUT_OFFSET_DB_L, R: CONFIG.INPUT_OFFSET_DB_R });
|
||
if (opts.persist) saveConfig();
|
||
return id;
|
||
}
|
||
|
||
function updateVuReference() {
|
||
const digitalRef = Number.isFinite(CONFIG.DIGITAL_REF_DBFS_RMS) ? CONFIG.DIGITAL_REF_DBFS_RMS : -18;
|
||
const offset = CONFIG.VU_REF_IS_PLUS4 ? 4 : 0;
|
||
CONFIG.VU_DBFS_REF = digitalRef + offset;
|
||
}
|
||
|
||
function updateInputOffsetGain(dbValue = { L: CONFIG.INPUT_OFFSET_DB_L, R: CONFIG.INPUT_OFFSET_DB_R }) {
|
||
const clampDb = (raw, fallback) => {
|
||
let val = Number(raw);
|
||
if (!Number.isFinite(val)) val = Number(fallback);
|
||
if (!Number.isFinite(val)) val = -5;
|
||
return Math.max(INPUT_OFFSET_DB_MIN, Math.min(INPUT_OFFSET_DB_MAX, val));
|
||
};
|
||
|
||
let dbL, dbR;
|
||
if (dbValue && typeof dbValue === 'object') {
|
||
dbL = clampDb(dbValue.L ?? dbValue.left ?? CONFIG.INPUT_OFFSET_DB_L, CONFIG.INPUT_OFFSET_DB_L);
|
||
dbR = clampDb(dbValue.R ?? dbValue.right ?? CONFIG.INPUT_OFFSET_DB_R, CONFIG.INPUT_OFFSET_DB_R);
|
||
} else {
|
||
const both = clampDb(dbValue, CONFIG.INPUT_OFFSET_DB_L);
|
||
dbL = both;
|
||
dbR = both;
|
||
}
|
||
|
||
CONFIG.INPUT_OFFSET_DB_L = dbL;
|
||
CONFIG.INPUT_OFFSET_DB_R = dbR;
|
||
CONFIG.INPUT_OFFSET_GAIN_L = Math.pow(10, dbL / 20);
|
||
CONFIG.INPUT_OFFSET_GAIN_R = Math.pow(10, dbR / 20);
|
||
return { L: CONFIG.INPUT_OFFSET_GAIN_L, R: CONFIG.INPUT_OFFSET_GAIN_R };
|
||
}
|
||
|
||
function applyAlignmentProfile(mode = 'ebu', opts = {}) {
|
||
const key = (mode === 'ard' || mode === 'al_minus9') ? 'ard'
|
||
: (mode === 'ebu' || mode === 'al_minus6') ? 'ebu'
|
||
: 'ebu';
|
||
const profile = ALIGNMENT_PROFILES[key] || ALIGNMENT_PROFILES.ebu;
|
||
const digitalRef = profile.digitalRefRms;
|
||
|
||
CONFIG.ALIGNMENT_MODE = profile.id;
|
||
CONFIG.PPM_DIN_MODE = profile.dinMode;
|
||
CONFIG.DIGITAL_REF_DBFS_RMS = digitalRef;
|
||
CONFIG.PPM_REF_DBFS_PEAK_FOR_0_DBU = digitalRef + 3;
|
||
CONFIG.RMS_REF_DBFS_FOR_REF_DBU = digitalRef;
|
||
CONFIG.RMS_MODE = 'dbfs';
|
||
CONFIG.RMS_REF_DBU = 0;
|
||
updateVuReference();
|
||
|
||
if (!opts.skipDinStorage && typeof localStorage !== 'undefined') {
|
||
try { localStorage.setItem('ppm_din_mode', CONFIG.PPM_DIN_MODE); }
|
||
catch (_) {}
|
||
}
|
||
if (opts.persist) saveConfig();
|
||
return profile.id;
|
||
}
|
||
|
||
function loadConfig(opts = {}) {
|
||
const force = !!opts.force;
|
||
if (configLoadedOnce && !force) return;
|
||
configLoadedOnce = true;
|
||
let saved = null;
|
||
try {
|
||
const raw = localStorage.getItem(STORAGE_KEY);
|
||
if (!raw) return;
|
||
saved = JSON.parse(raw);
|
||
for (const k of Object.keys(saved)) if (k in CONFIG) CONFIG[k] = saved[k];
|
||
if (typeof saved.PPM_TOP === 'number' && !('PPM_EBU_TOP' in saved)) CONFIG.PPM_EBU_TOP = saved.PPM_TOP;
|
||
if (typeof saved.PPM_BOTTOM === 'number' && !('PPM_EBU_BOTTOM' in saved)) CONFIG.PPM_EBU_BOTTOM = saved.PPM_BOTTOM;
|
||
if (typeof saved.PPM_RED_START === 'number' && !('PPM_EBU_RED_START' in saved)) CONFIG.PPM_EBU_RED_START = saved.PPM_RED_START;
|
||
if (typeof saved.PPM_DIN_TOP === 'number') CONFIG.PPM_DIN_TOP = saved.PPM_DIN_TOP;
|
||
if (typeof saved.PPM_DIN_BOTTOM === 'number') CONFIG.PPM_DIN_BOTTOM = saved.PPM_DIN_BOTTOM;
|
||
if (typeof saved.PPM_DIN_RED_START === 'number') CONFIG.PPM_DIN_RED_START = saved.PPM_DIN_RED_START;
|
||
if (typeof saved.PPM_DIN_ATTACK_MS === 'number') CONFIG.PPM_DIN_ATTACK_MS = saved.PPM_DIN_ATTACK_MS;
|
||
if (typeof saved.PPM_DIN_DECAY_DB_PER_S === 'number') CONFIG.PPM_DIN_DECAY_DB_PER_S = saved.PPM_DIN_DECAY_DB_PER_S;
|
||
if (typeof saved.PPM_DIN_HOLD_MS === 'number') CONFIG.PPM_DIN_HOLD_MS = saved.PPM_DIN_HOLD_MS;
|
||
if (CONFIG.RTA_BAR_LAYOUT === 'classic') CONFIG.RTA_BAR_LAYOUT = 'rtw';
|
||
if (!Number.isFinite(CONFIG.DBFS_TOP)) CONFIG.DBFS_TOP = CONFIG_DEFAULTS.DBFS_TOP;
|
||
if (!Number.isFinite(CONFIG.DBFS_BOTTOM)) CONFIG.DBFS_BOTTOM = CONFIG_DEFAULTS.DBFS_BOTTOM;
|
||
// Clamp to sensible bounds while keeping the user-selected display range.
|
||
CONFIG.DBFS_TOP = Math.max(9, Math.min(24, CONFIG.DBFS_TOP));
|
||
CONFIG.DBFS_BOTTOM = Math.max(-160, Math.min(CONFIG.DBFS_TOP - 1, CONFIG.DBFS_BOTTOM));
|
||
|
||
if (!Number.isFinite(CONFIG.SPECTRO_GAMMA)) CONFIG.SPECTRO_GAMMA = CONFIG_DEFAULTS.SPECTRO_GAMMA;
|
||
CONFIG.SPECTRO_GAMMA = Math.max(0.3, Math.min(1.2, CONFIG.SPECTRO_GAMMA));
|
||
if (!Number.isFinite(CONFIG.WAVEFORM_WINDOW_SEC)) CONFIG.WAVEFORM_WINDOW_SEC = CONFIG_DEFAULTS.WAVEFORM_WINDOW_SEC;
|
||
CONFIG.WAVEFORM_WINDOW_SEC = Math.max(0.05, Math.min(15, CONFIG.WAVEFORM_WINDOW_SEC));
|
||
CONFIG.WAVEFORM_MODE = (CONFIG.WAVEFORM_MODE === 'overlay' || CONFIG.WAVEFORM_MODE === 'diff' || CONFIG.WAVEFORM_MODE === 'stacked')
|
||
? CONFIG.WAVEFORM_MODE
|
||
: CONFIG_DEFAULTS.WAVEFORM_MODE;
|
||
|
||
// UI/layout: keep user value, clamp to avoid NaN/layout issues.
|
||
if (!Number.isFinite(CONFIG.AXIS_GUTTER_LEFT)) CONFIG.AXIS_GUTTER_LEFT = CONFIG_DEFAULTS.AXIS_GUTTER_LEFT;
|
||
CONFIG.AXIS_GUTTER_LEFT = Math.max(8, Math.min(60, CONFIG.AXIS_GUTTER_LEFT));
|
||
|
||
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 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
|
||
|| 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) localStorage.setItem(VU_OFFSET_ALIGNMENT_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.LUFS_I_NORM_ENABLED = !!CONFIG.LUFS_I_NORM_ENABLED;
|
||
CONFIG.STOPWATCH_DISPLAY_STYLE = (CONFIG.STOPWATCH_DISPLAY_STYLE === 'seven') ? 'seven' : 'mono';
|
||
if (!Number.isFinite(CONFIG.STOPWATCH_AUTO_THRESHOLD_DBFS)) CONFIG.STOPWATCH_AUTO_THRESHOLD_DBFS = CONFIG_DEFAULTS.STOPWATCH_AUTO_THRESHOLD_DBFS;
|
||
CONFIG.STOPWATCH_AUTO_THRESHOLD_DBFS = Math.max(-120, Math.min(20, Number(CONFIG.STOPWATCH_AUTO_THRESHOLD_DBFS)));
|
||
try {
|
||
const needsMigration = !localStorage.getItem(LR_DELAY_ZERO_MIGRATION_KEY);
|
||
const savedDelay = Number(saved?.LR_FRACTIONAL_DELAY_SAMPLES);
|
||
if (needsMigration && Number.isFinite(savedDelay) && savedDelay === 0) {
|
||
CONFIG.LR_FRACTIONAL_DELAY_SAMPLES = 0.996;
|
||
localStorage.setItem(LR_DELAY_ZERO_MIGRATION_KEY, '1');
|
||
}
|
||
} catch (_) {}
|
||
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;
|
||
CONFIG.HIFI_PEAK_ALIGNMENT = (CONFIG.HIFI_PEAK_ALIGNMENT === 'ppm_din_zero' || CONFIG.HIFI_PEAK_ALIGNMENT === 'dbfs_zero')
|
||
? 'ppm_din_zero'
|
||
: 'ppm_din_minus5';
|
||
if (!Number.isFinite(CONFIG.RECORD_AUTO_THRESHOLD_DBFS)) CONFIG.RECORD_AUTO_THRESHOLD_DBFS = CONFIG_DEFAULTS.RECORD_AUTO_THRESHOLD_DBFS;
|
||
CONFIG.RECORD_AUTO_THRESHOLD_DBFS = Math.max(-120, Math.min(20, Number(CONFIG.RECORD_AUTO_THRESHOLD_DBFS)));
|
||
CONFIG.SOFTWARE_PRESET = normalizeSoftwarePresetId(CONFIG.SOFTWARE_PRESET);
|
||
if (typeof CONFIG.UI_LAST_STYLE !== 'string') CONFIG.UI_LAST_STYLE = CONFIG_DEFAULTS.UI_LAST_STYLE;
|
||
if (typeof CONFIG.UI_LAST_NON_OPTION_STYLE !== 'string') CONFIG.UI_LAST_NON_OPTION_STYLE = CONFIG_DEFAULTS.UI_LAST_NON_OPTION_STYLE;
|
||
|
||
// Split-View: normalize ids/counts to avoid invalid config values.
|
||
const normSplitPlot = (v, fallback) => {
|
||
return (
|
||
v === 'phase-wheel' ||
|
||
v === 'realtime' ||
|
||
v === 'goniometer-rtw' ||
|
||
v === 'peak-history' ||
|
||
v === 'classic-needles' ||
|
||
v === 'panel' ||
|
||
v === 'clock' ||
|
||
v === 'none'
|
||
) ? v : fallback;
|
||
};
|
||
const normSplitCount3 = (v) => {
|
||
const n = Number(v);
|
||
if (!Number.isFinite(n)) return 0;
|
||
return Math.max(0, Math.min(3, n | 0));
|
||
};
|
||
const normSplitMeter = (v, fallback) => {
|
||
const id = String(v || '');
|
||
const ok = (id === 'none' || id === 'vu' || id === 'ppm-ebu' || id === 'ppm-din' || id === 'tp' || id === 'hifi-peak' || id === 'rms' || id === 'lufs' || id === 'stopwatch');
|
||
return ok ? id : fallback;
|
||
};
|
||
const normSplitPos = (v, fallback) => {
|
||
const id = String(v || '');
|
||
return (id === 'left' || id === 'center' || id === 'right') ? id : fallback;
|
||
};
|
||
CONFIG.SPLIT_VIEW_LEFT = normSplitPlot(CONFIG.SPLIT_VIEW_LEFT, 'phase-wheel');
|
||
CONFIG.SPLIT_VIEW_RIGHT = normSplitPlot(CONFIG.SPLIT_VIEW_RIGHT, 'realtime');
|
||
CONFIG.SPLIT_VIEW_TAP_ACTION = (CONFIG.SPLIT_VIEW_TAP_ACTION === 'popup') ? 'popup' : 'switch';
|
||
CONFIG.QUAD_VIEW_TL = normSplitPlot(CONFIG.QUAD_VIEW_TL, 'phase-wheel');
|
||
CONFIG.QUAD_VIEW_TR = normSplitPlot(CONFIG.QUAD_VIEW_TR, 'realtime');
|
||
CONFIG.QUAD_VIEW_BL = normSplitPlot(CONFIG.QUAD_VIEW_BL, 'goniometer-rtw');
|
||
CONFIG.QUAD_VIEW_BR = normSplitPlot(CONFIG.QUAD_VIEW_BR, 'none');
|
||
CONFIG.QUAD_VIEW_TAP_ACTION = (CONFIG.QUAD_VIEW_TAP_ACTION === 'popup') ? 'popup' : 'switch';
|
||
CONFIG.QUAD_VIEW_METER_COUNT = normSplitCount3(CONFIG.QUAD_VIEW_METER_COUNT);
|
||
CONFIG.QUAD_VIEW_METER_1 = normSplitMeter(CONFIG.QUAD_VIEW_METER_1, 'vu');
|
||
CONFIG.QUAD_VIEW_METER_2 = normSplitMeter(CONFIG.QUAD_VIEW_METER_2, 'ppm-din');
|
||
CONFIG.QUAD_VIEW_METER_3 = normSplitMeter(CONFIG.QUAD_VIEW_METER_3, 'lufs');
|
||
CONFIG.QUAD_VIEW_METER_1_POS = normSplitPos(CONFIG.QUAD_VIEW_METER_1_POS, 'right');
|
||
CONFIG.QUAD_VIEW_METER_2_POS = normSplitPos(CONFIG.QUAD_VIEW_METER_2_POS, 'right');
|
||
CONFIG.QUAD_VIEW_METER_3_POS = normSplitPos(CONFIG.QUAD_VIEW_METER_3_POS, 'right');
|
||
CONFIG.SPLIT_VIEW_METER_COUNT = normSplitCount3(CONFIG.SPLIT_VIEW_METER_COUNT);
|
||
CONFIG.SPLIT_VIEW_METER_1 = normSplitMeter(CONFIG.SPLIT_VIEW_METER_1, 'vu');
|
||
CONFIG.SPLIT_VIEW_METER_2 = normSplitMeter(CONFIG.SPLIT_VIEW_METER_2, 'ppm-din');
|
||
CONFIG.SPLIT_VIEW_METER_3 = normSplitMeter(CONFIG.SPLIT_VIEW_METER_3, 'lufs');
|
||
CONFIG.SPLIT_VIEW_METER_1_POS = normSplitPos(CONFIG.SPLIT_VIEW_METER_1_POS, 'right');
|
||
CONFIG.SPLIT_VIEW_METER_2_POS = normSplitPos(CONFIG.SPLIT_VIEW_METER_2_POS, 'right');
|
||
CONFIG.SPLIT_VIEW_METER_3_POS = normSplitPos(CONFIG.SPLIT_VIEW_METER_3_POS, 'right');
|
||
|
||
// Migration (older Split-View config): per-side meters → global 3-slot model.
|
||
const hasNew = saved && ('SPLIT_VIEW_METER_COUNT' in saved);
|
||
const hasOld = saved && ('SPLIT_VIEW_LEFT_METER_COUNT' in saved || 'SPLIT_VIEW_RIGHT_METER_COUNT' in saved);
|
||
if (!hasNew && hasOld) {
|
||
const oldClamp2 = (v) => {
|
||
const n = Number(v);
|
||
if (!Number.isFinite(n)) return 0;
|
||
return Math.max(0, Math.min(2, n | 0));
|
||
};
|
||
const lCount = oldClamp2(saved?.SPLIT_VIEW_LEFT_METER_COUNT);
|
||
const rCount = oldClamp2(saved?.SPLIT_VIEW_RIGHT_METER_COUNT);
|
||
const slots = [];
|
||
if (lCount >= 1) slots.push({ id: normSplitMeter(saved?.SPLIT_VIEW_LEFT_METER_1, 'vu'), pos: 'left' });
|
||
if (lCount >= 2) slots.push({ id: normSplitMeter(saved?.SPLIT_VIEW_LEFT_METER_2, 'ppm-din'), pos: 'left' });
|
||
if (rCount >= 1) slots.push({ id: normSplitMeter(saved?.SPLIT_VIEW_RIGHT_METER_1, 'vu'), pos: 'right' });
|
||
if (rCount >= 2) slots.push({ id: normSplitMeter(saved?.SPLIT_VIEW_RIGHT_METER_2, 'ppm-din'), pos: 'right' });
|
||
CONFIG.SPLIT_VIEW_METER_COUNT = Math.max(0, Math.min(3, slots.length));
|
||
if (slots[0]) { CONFIG.SPLIT_VIEW_METER_1 = slots[0].id; CONFIG.SPLIT_VIEW_METER_1_POS = slots[0].pos; }
|
||
if (slots[1]) { CONFIG.SPLIT_VIEW_METER_2 = slots[1].id; CONFIG.SPLIT_VIEW_METER_2_POS = slots[1].pos; }
|
||
if (slots[2]) { CONFIG.SPLIT_VIEW_METER_3 = slots[2].id; CONFIG.SPLIT_VIEW_METER_3_POS = slots[2].pos; }
|
||
}
|
||
|
||
// Derived state; keep it deterministic.
|
||
CONFIG.Y_TICKS = [];
|
||
if (!Number.isFinite(CONFIG.GONIO_MANUAL_GAIN_DB)) {
|
||
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));
|
||
}
|
||
} catch (e) {
|
||
console.warn('Config load error:', e);
|
||
}
|
||
const loadedMode = (CONFIG.ALIGNMENT_MODE === 'ard' || CONFIG.PPM_DIN_MODE === 'al_minus9') ? 'ard' : 'ebu';
|
||
applyAlignmentProfile(loadedMode, { persist: false, skipDinStorage: true });
|
||
|
||
// Migration: older configs only stored INPUT_OFFSET_DB.
|
||
const hasPerChannelOffset = !!(saved && ('INPUT_OFFSET_DB_L' in saved || 'INPUT_OFFSET_DB_R' in saved));
|
||
if (!hasPerChannelOffset && Number.isFinite(saved?.INPUT_OFFSET_DB)) {
|
||
CONFIG.INPUT_OFFSET_DB_L = saved.INPUT_OFFSET_DB;
|
||
CONFIG.INPUT_OFFSET_DB_R = saved.INPUT_OFFSET_DB;
|
||
}
|
||
if (!(saved && ('RECORD_PI_TARGET' in saved))) {
|
||
CONFIG.RECORD_PI_TARGET = (CONFIG.RECORD_TARGET === 'volumio') ? 'volumio' : 'analyzer';
|
||
}
|
||
updateInputOffsetGain({
|
||
L: CONFIG.INPUT_OFFSET_DB_L,
|
||
R: CONFIG.INPUT_OFFSET_DB_R,
|
||
});
|
||
try { saveConfig(); } catch (_) {}
|
||
}
|
||
|
||
function saveConfig() {
|
||
try {
|
||
localStorage.setItem(STORAGE_KEY, JSON.stringify(CONFIG));
|
||
} catch (e) {
|
||
console.warn('Config save error:', e);
|
||
}
|
||
}
|
||
|
||
function buildPhoenixGlobalConfigPayload() {
|
||
const rawFft = Number(CONFIG.FFT_SIZE);
|
||
const fftSize = (rawFft === 2048 || rawFft === 4096 || rawFft === 8192 || rawFft === 16384)
|
||
? rawFft
|
||
: (rawFft < 3072 ? 2048 : (rawFft < 6144 ? 4096 : (rawFft < 12288 ? 8192 : 16384)));
|
||
const rawXyPoints = Number(CONFIG.XY_POINTS);
|
||
const xyPoints = [128, 256, 512, 1024, 2048].includes(rawXyPoints)
|
||
? rawXyPoints
|
||
: (rawXyPoints < 192 ? 128 : (rawXyPoints < 384 ? 256 : (rawXyPoints < 768 ? 512 : (rawXyPoints < 1536 ? 1024 : 2048))));
|
||
const normalizeColor = (value, fallback) => {
|
||
const color = String(value || '').trim();
|
||
return color || fallback;
|
||
};
|
||
const normalizeMeterSlot = (value, fallback) => {
|
||
const raw = String(value || '').trim().toLowerCase();
|
||
if (raw === 'vu') return 'vu';
|
||
if (raw === 'ppm' || raw === 'ppm-ebu' || raw === 'ppm_ebu') return 'ppm-ebu';
|
||
if (raw === 'ppm-din' || raw === 'ppm_din') return 'ppm-din';
|
||
if (raw === 'tp') return 'tp';
|
||
if (raw === 'hifi-peak' || raw === 'hifi_peak') return 'hifi-peak';
|
||
if (raw === 'rms') return 'rms';
|
||
if (raw === 'lufs') return 'lufs';
|
||
if (raw === 'stopwatch') return 'stopwatch';
|
||
return fallback;
|
||
};
|
||
const normalizePeakHistoryScroll = (value) => {
|
||
const allowed = new Set([0.5, 1, 2, 4, 6]);
|
||
const num = Number(value);
|
||
return allowed.has(num) ? num : 1;
|
||
};
|
||
const normalizePhaseAmplitudeMode = (value) => {
|
||
return String(value || '').trim().toLowerCase() === 'ppm-din' ? 'ppm-din' : 'bandpass';
|
||
};
|
||
const normalizeRtaBpoMode = (value) => {
|
||
const mode = String(value || '').trim();
|
||
return (mode === '1_3' || mode === '1_6' || mode === '1_12') ? mode : '1_6';
|
||
};
|
||
const phaseAmplitudeMode = normalizePhaseAmplitudeMode(CONFIG.PHASE_AMPLITUDE_MODE);
|
||
return {
|
||
fftSize,
|
||
rtaBpoMode: normalizeRtaBpoMode(CONFIG.RTA_BPO_MODE),
|
||
inputOffsetDbL: Number.isFinite(CONFIG.INPUT_OFFSET_DB_L) ? CONFIG.INPUT_OFFSET_DB_L : -5,
|
||
inputOffsetDbR: Number.isFinite(CONFIG.INPUT_OFFSET_DB_R) ? CONFIG.INPUT_OFFSET_DB_R : -5,
|
||
monoInput: !!CONFIG.MONO_INPUT,
|
||
lrFractionalDelayEnabled: !!CONFIG.LR_FRACTIONAL_DELAY_ENABLED,
|
||
lrFractionalDelaySamples: Number.isFinite(CONFIG.LR_FRACTIONAL_DELAY_SAMPLES) ? CONFIG.LR_FRACTIONAL_DELAY_SAMPLES : (CONFIG_DEFAULTS.LR_FRACTIONAL_DELAY_SAMPLES ?? 0.996),
|
||
ppmDinAttackMs: Number.isFinite(CONFIG.PPM_DIN_ATTACK_MS) ? CONFIG.PPM_DIN_ATTACK_MS : (CONFIG_DEFAULTS.PPM_DIN_ATTACK_MS ?? 5),
|
||
ppmDinDecayDbPerS: Number.isFinite(CONFIG.PPM_DIN_DECAY_DB_PER_S) ? CONFIG.PPM_DIN_DECAY_DB_PER_S : (CONFIG_DEFAULTS.PPM_DIN_DECAY_DB_PER_S ?? 11.8),
|
||
ppmDinFastAttack: !!CONFIG.PPM_DIN_FAST_ATTACK,
|
||
ppmEbuAttackMs: Number.isFinite(CONFIG.PPM_EBU_ATTACK_MS) ? CONFIG.PPM_EBU_ATTACK_MS : (CONFIG_DEFAULTS.PPM_EBU_ATTACK_MS ?? 10),
|
||
ppmEbuDecayDbPerS: Number.isFinite(CONFIG.PPM_EBU_DECAY_DB_PER_S) ? CONFIG.PPM_EBU_DECAY_DB_PER_S : (CONFIG_DEFAULTS.PPM_EBU_DECAY_DB_PER_S ?? 8.6),
|
||
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,
|
||
ppmDinLoudnessOffsetDb: Number.isFinite(CONFIG.PPM_DIN_LOUDNESS_OFFSET_DB) ? CONFIG.PPM_DIN_LOUDNESS_OFFSET_DB : (CONFIG_DEFAULTS.PPM_DIN_LOUDNESS_OFFSET_DB ?? 0),
|
||
xyPoints,
|
||
gonioDisplayGainDb: Number.isFinite(CONFIG.GONIO_DISPLAY_GAIN_DB) ? CONFIG.GONIO_DISPLAY_GAIN_DB : (CONFIG_DEFAULTS.GONIO_DISPLAY_GAIN_DB ?? -5),
|
||
recordOutputFormat: (CONFIG.RECORD_OUTPUT_FORMAT === 'webm') ? 'webm' : ((CONFIG.RECORD_OUTPUT_FORMAT === 'mp3') ? 'mp3' : 'wav'),
|
||
recordMp3BitrateKbps: [64, 128, 192, 256, 320].includes(Number(CONFIG.RECORD_MP3_BITRATE_KBPS)) ? Number(CONFIG.RECORD_MP3_BITRATE_KBPS) : 192,
|
||
recordTarget: (CONFIG.RECORD_TARGET === 'analyzer' || CONFIG.RECORD_TARGET === 'volumio')
|
||
? CONFIG.RECORD_TARGET
|
||
: ((CONFIG.RECORD_PI_TARGET === 'volumio') ? 'volumio' : 'analyzer'),
|
||
recordAutoSplitGapSec: Number.isFinite(CONFIG.RECORD_AUTO_SPLIT_GAP_SEC) ? CONFIG.RECORD_AUTO_SPLIT_GAP_SEC : (CONFIG_DEFAULTS.RECORD_AUTO_SPLIT_GAP_SEC ?? 1.5),
|
||
recordAutoThresholdDbfs: Number.isFinite(CONFIG.RECORD_AUTO_THRESHOLD_DBFS) ? CONFIG.RECORD_AUTO_THRESHOLD_DBFS : (CONFIG_DEFAULTS.RECORD_AUTO_THRESHOLD_DBFS ?? -50),
|
||
screensaverEnabled: CONFIG.SCREENSAVER_ENABLED !== false,
|
||
screensaverIdleMin: Number.isFinite(CONFIG.SCREENSAVER_IDLE_MIN) ? CONFIG.SCREENSAVER_IDLE_MIN : (CONFIG_DEFAULTS.SCREENSAVER_IDLE_MIN ?? 5),
|
||
screensaverActivityDb: Number.isFinite(CONFIG.SCREENSAVER_ACTIVITY_DB) ? CONFIG.SCREENSAVER_ACTIVITY_DB : (CONFIG_DEFAULTS.SCREENSAVER_ACTIVITY_DB ?? -50),
|
||
screensaverLedGlow: CONFIG.SCREENSAVER_LED_GLOW !== false,
|
||
screensaverLedColor: normalizeColor(CONFIG.SCREENSAVER_LED_COLOR, CONFIG_DEFAULTS.SCREENSAVER_LED_COLOR || '#ff0000'),
|
||
clockLedColor: normalizeColor(CONFIG.CLOCK_LED_COLOR, CONFIG_DEFAULTS.CLOCK_LED_COLOR || '#ff0000'),
|
||
headerTextColor: normalizeColor(CONFIG.HEADER_TEXT_COLOR, CONFIG_DEFAULTS.HEADER_TEXT_COLOR || '#ffe066'),
|
||
rtaBarBaseColor: normalizeColor(CONFIG.RTA_BAR_BASE_COLOR, CONFIG_DEFAULTS.RTA_BAR_BASE_COLOR || '#ffe066'),
|
||
peakHistoryScrollMode: normalizePeakHistoryScroll(CONFIG.PEAK_HISTORY_SCROLL_MODE),
|
||
peakHistoryFillEnabled: !!CONFIG.PEAK_HISTORY_FILL_ENABLED,
|
||
peakHistoryFillInvert: !!CONFIG.PEAK_HISTORY_FILL_INVERT,
|
||
peakHistorySlot: normalizeMeterSlot(CONFIG.PEAK_HISTORY_SLOT, CONFIG_DEFAULTS.PEAK_HISTORY_SLOT || 'ppm-din'),
|
||
phaseDisplayGainDb: Number.isFinite(CONFIG.PHASE_DISPLAY_GAIN_DB) ? CONFIG.PHASE_DISPLAY_GAIN_DB : (CONFIG_DEFAULTS.PHASE_DISPLAY_GAIN_DB ?? 0),
|
||
phaseAgcEnabled: phaseAmplitudeMode === 'ppm-din' ? false : !!CONFIG.PHASE_AGC_ENABLED,
|
||
phaseTrailEnabled: !!CONFIG.PHASE_TRAIL_ENABLED,
|
||
phaseAmplitudeMode,
|
||
classicNeedlesSlot: normalizeMeterSlot(CONFIG.CLASSIC_NEEDLES_SLOT, CONFIG_DEFAULTS.CLASSIC_NEEDLES_SLOT || 'vu'),
|
||
waveformColorLeft: normalizeColor(CONFIG.WAVEFORM_COLOR_LEFT, CONFIG_DEFAULTS.WAVEFORM_COLOR_LEFT || '#00e7ff'),
|
||
waveformColorRight: normalizeColor(CONFIG.WAVEFORM_COLOR_RIGHT, CONFIG_DEFAULTS.WAVEFORM_COLOR_RIGHT || '#ff6b81'),
|
||
waveformColorDiff: normalizeColor(CONFIG.WAVEFORM_COLOR_DIFF, CONFIG_DEFAULTS.WAVEFORM_COLOR_DIFF || '#00e7ff'),
|
||
vuColorNormal: normalizeColor(CONFIG.VU_COLOR_NORMAL, CONFIG_DEFAULTS.VU_COLOR_NORMAL || '#ffe066'),
|
||
vuColorWarn: normalizeColor(CONFIG.VU_COLOR_WARN, CONFIG_DEFAULTS.VU_COLOR_WARN || '#ff3b3b'),
|
||
ppmDinColorNormal: normalizeColor(CONFIG.PPM_DIN_COLOR_NORMAL, CONFIG_DEFAULTS.PPM_DIN_COLOR_NORMAL || '#ffe066'),
|
||
ppmDinColorWarn: normalizeColor(CONFIG.PPM_DIN_COLOR_WARN, CONFIG_DEFAULTS.PPM_DIN_COLOR_WARN || '#ff3b3b'),
|
||
ppmEbuColorNormal: normalizeColor(CONFIG.PPM_EBU_COLOR_NORMAL, CONFIG_DEFAULTS.PPM_EBU_COLOR_NORMAL || '#ffe066'),
|
||
ppmEbuColorWarn: normalizeColor(CONFIG.PPM_EBU_COLOR_WARN, CONFIG_DEFAULTS.PPM_EBU_COLOR_WARN || '#ff3b3b'),
|
||
tpColorNormal: normalizeColor(CONFIG.TP_COLOR_NORMAL, CONFIG_DEFAULTS.TP_COLOR_NORMAL || '#ffe066'),
|
||
tpColorWarn: normalizeColor(CONFIG.TP_COLOR_WARN, CONFIG_DEFAULTS.TP_COLOR_WARN || '#ff3b3b'),
|
||
rmsColorNormal: normalizeColor(CONFIG.RMS_COLOR_NORMAL, CONFIG_DEFAULTS.RMS_COLOR_NORMAL || '#34d399'),
|
||
rmsColorWarn: normalizeColor(CONFIG.RMS_COLOR_WARN, CONFIG_DEFAULTS.RMS_COLOR_WARN || '#ff3b3b'),
|
||
lufsColorI: normalizeColor(CONFIG.LUFS_COLOR_I, CONFIG_DEFAULTS.LUFS_COLOR_I || '#34d399'),
|
||
lufsColorM: normalizeColor(CONFIG.LUFS_COLOR_M, CONFIG_DEFAULTS.LUFS_COLOR_M || '#ffe066'),
|
||
lufsColorS: normalizeColor(CONFIG.LUFS_COLOR_S, CONFIG_DEFAULTS.LUFS_COLOR_S || '#ff3b3b'),
|
||
lufsScaleLabelColor: normalizeColor(CONFIG.LUFS_SCALE_LABEL_COLOR, CONFIG_DEFAULTS.LUFS_SCALE_LABEL_COLOR || '#8fd3d4'),
|
||
};
|
||
}
|
||
|
||
function applyPhoenixGlobalConfig(payload = {}) {
|
||
if (!payload || typeof payload !== 'object') return null;
|
||
const normalizeColor = (value, fallback) => {
|
||
const color = String(value || '').trim();
|
||
return color || fallback;
|
||
};
|
||
const normalizeMeterSlot = (value, fallback) => {
|
||
const raw = String(value || '').trim().toLowerCase();
|
||
if (raw === 'vu') return 'vu';
|
||
if (raw === 'ppm' || raw === 'ppm-ebu' || raw === 'ppm_ebu') return 'ppm-ebu';
|
||
if (raw === 'ppm-din' || raw === 'ppm_din') return 'ppm-din';
|
||
if (raw === 'tp') return 'tp';
|
||
if (raw === 'hifi-peak' || raw === 'hifi_peak') return 'hifi-peak';
|
||
if (raw === 'rms') return 'rms';
|
||
if (raw === 'lufs') return 'lufs';
|
||
if (raw === 'stopwatch') return 'stopwatch';
|
||
return fallback;
|
||
};
|
||
const fft = Number(payload.fftSize);
|
||
if (Number.isFinite(fft)) {
|
||
CONFIG.FFT_SIZE = (fft === 2048 || fft === 4096 || fft === 8192 || fft === 16384)
|
||
? fft
|
||
: (fft < 3072 ? 2048 : (fft < 6144 ? 4096 : (fft < 12288 ? 8192 : 16384)));
|
||
}
|
||
if ('rtaBpoMode' in payload) {
|
||
const mode = String(payload.rtaBpoMode || '').trim();
|
||
CONFIG.RTA_BPO_MODE = (mode === '1_3' || mode === '1_6' || mode === '1_12') ? mode : '1_6';
|
||
}
|
||
const dinAttack = Number(payload.ppmDinAttackMs);
|
||
if (Number.isFinite(dinAttack)) CONFIG.PPM_DIN_ATTACK_MS = Math.max(0.1, Math.min(100, dinAttack));
|
||
const dinDecay = Number(payload.ppmDinDecayDbPerS);
|
||
if (Number.isFinite(dinDecay)) CONFIG.PPM_DIN_DECAY_DB_PER_S = Math.max(0.1, Math.min(120, dinDecay));
|
||
if ('ppmDinFastAttack' in payload) CONFIG.PPM_DIN_FAST_ATTACK = !!payload.ppmDinFastAttack;
|
||
const ebuAttack = Number(payload.ppmEbuAttackMs);
|
||
if (Number.isFinite(ebuAttack)) CONFIG.PPM_EBU_ATTACK_MS = Math.max(0.1, Math.min(100, ebuAttack));
|
||
const ebuDecay = Number(payload.ppmEbuDecayDbPerS);
|
||
if (Number.isFinite(ebuDecay)) CONFIG.PPM_EBU_DECAY_DB_PER_S = Math.max(0.1, Math.min(120, ebuDecay));
|
||
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;
|
||
if ('ppmDinLoudnessBoxes' in payload) CONFIG.PPM_DIN_LOUDNESS_BOXES = !!payload.ppmDinLoudnessBoxes;
|
||
const ppmDinLoudnessOffsetDb = Number(payload.ppmDinLoudnessOffsetDb);
|
||
if (Number.isFinite(ppmDinLoudnessOffsetDb)) CONFIG.PPM_DIN_LOUDNESS_OFFSET_DB = Math.max(-7, Math.min(7, ppmDinLoudnessOffsetDb));
|
||
const xyPoints = Number(payload.xyPoints);
|
||
if (Number.isFinite(xyPoints)) {
|
||
CONFIG.XY_POINTS = [128, 256, 512, 1024, 2048].includes(xyPoints)
|
||
? xyPoints
|
||
: (xyPoints < 192 ? 128 : (xyPoints < 384 ? 256 : (xyPoints < 768 ? 512 : (xyPoints < 1536 ? 1024 : 2048))));
|
||
}
|
||
const gonioDisplayGainDb = Number(payload.gonioDisplayGainDb);
|
||
if (Number.isFinite(gonioDisplayGainDb)) {
|
||
CONFIG.GONIO_DISPLAY_GAIN_DB = Math.max(-35, Math.min(35, Math.round(gonioDisplayGainDb / 5) * 5));
|
||
}
|
||
if ('recordOutputFormat' in payload) {
|
||
const fmt = String(payload.recordOutputFormat || '').toLowerCase();
|
||
CONFIG.RECORD_OUTPUT_FORMAT = (fmt === 'webm') ? 'webm' : ((fmt === 'mp3') ? 'mp3' : 'wav');
|
||
}
|
||
const mp3Rate = Number(payload.recordMp3BitrateKbps);
|
||
if ([64, 128, 192, 256, 320].includes(mp3Rate)) CONFIG.RECORD_MP3_BITRATE_KBPS = mp3Rate;
|
||
if ('recordTarget' in payload) {
|
||
const target = String(payload.recordTarget || '').toLowerCase();
|
||
CONFIG.RECORD_PI_TARGET = (target === 'volumio') ? 'volumio' : 'analyzer';
|
||
if (CONFIG.RECORD_TARGET !== 'download') {
|
||
CONFIG.RECORD_TARGET = CONFIG.RECORD_PI_TARGET;
|
||
}
|
||
}
|
||
const autoGap = Number(payload.recordAutoSplitGapSec);
|
||
if (Number.isFinite(autoGap)) CONFIG.RECORD_AUTO_SPLIT_GAP_SEC = Math.max(0.5, Math.min(3, Math.round(autoGap * 2) / 2));
|
||
const autoThr = Number(payload.recordAutoThresholdDbfs);
|
||
if (Number.isFinite(autoThr)) CONFIG.RECORD_AUTO_THRESHOLD_DBFS = Math.max(-120, Math.min(20, autoThr));
|
||
if ('screensaverEnabled' in payload) CONFIG.SCREENSAVER_ENABLED = !!payload.screensaverEnabled;
|
||
const idle = Number(payload.screensaverIdleMin);
|
||
if (Number.isFinite(idle)) CONFIG.SCREENSAVER_IDLE_MIN = Math.max(0, Math.min(120, idle));
|
||
const activity = Number(payload.screensaverActivityDb);
|
||
if (Number.isFinite(activity)) CONFIG.SCREENSAVER_ACTIVITY_DB = Math.max(-120, Math.min(0, activity));
|
||
if ('screensaverLedGlow' in payload) CONFIG.SCREENSAVER_LED_GLOW = !!payload.screensaverLedGlow;
|
||
if ('screensaverLedColor' in payload) {
|
||
CONFIG.SCREENSAVER_LED_COLOR = normalizeColor(payload.screensaverLedColor, CONFIG_DEFAULTS.SCREENSAVER_LED_COLOR || '#ff0000');
|
||
}
|
||
if ('clockLedColor' in payload) {
|
||
CONFIG.CLOCK_LED_COLOR = normalizeColor(payload.clockLedColor, CONFIG_DEFAULTS.CLOCK_LED_COLOR || '#ff0000');
|
||
}
|
||
if ('headerTextColor' in payload) {
|
||
CONFIG.HEADER_TEXT_COLOR = normalizeColor(payload.headerTextColor, CONFIG_DEFAULTS.HEADER_TEXT_COLOR || '#ffe066');
|
||
}
|
||
if ('rtaBarBaseColor' in payload) {
|
||
CONFIG.RTA_BAR_BASE_COLOR = normalizeColor(payload.rtaBarBaseColor, CONFIG_DEFAULTS.RTA_BAR_BASE_COLOR || '#ffe066');
|
||
}
|
||
const peakHistoryScrollMode = Number(payload.peakHistoryScrollMode);
|
||
if ([0.5, 1, 2, 4, 6].includes(peakHistoryScrollMode)) {
|
||
CONFIG.PEAK_HISTORY_SCROLL_MODE = peakHistoryScrollMode;
|
||
}
|
||
if ('peakHistoryFillEnabled' in payload) CONFIG.PEAK_HISTORY_FILL_ENABLED = !!payload.peakHistoryFillEnabled;
|
||
if ('peakHistoryFillInvert' in payload) CONFIG.PEAK_HISTORY_FILL_INVERT = !!payload.peakHistoryFillInvert;
|
||
if ('peakHistorySlot' in payload) {
|
||
CONFIG.PEAK_HISTORY_SLOT = normalizeMeterSlot(payload.peakHistorySlot, CONFIG_DEFAULTS.PEAK_HISTORY_SLOT || 'ppm-din');
|
||
}
|
||
const phaseDisplayGainDb = Number(payload.phaseDisplayGainDb);
|
||
if (Number.isFinite(phaseDisplayGainDb)) {
|
||
const clamped = Math.max(-35, Math.min(35, phaseDisplayGainDb));
|
||
CONFIG.PHASE_DISPLAY_GAIN_DB = Math.round(clamped / 5) * 5;
|
||
}
|
||
if ('phaseAmplitudeMode' in payload) {
|
||
CONFIG.PHASE_AMPLITUDE_MODE = String(payload.phaseAmplitudeMode || '').trim().toLowerCase() === 'ppm-din' ? 'ppm-din' : 'bandpass';
|
||
}
|
||
if ('phaseAgcEnabled' in payload) {
|
||
CONFIG.PHASE_AGC_ENABLED = (CONFIG.PHASE_AMPLITUDE_MODE === 'ppm-din') ? false : !!payload.phaseAgcEnabled;
|
||
}
|
||
if (CONFIG.PHASE_AMPLITUDE_MODE === 'ppm-din') {
|
||
CONFIG.PHASE_AGC_ENABLED = false;
|
||
} else if (CONFIG.PHASE_AGC_ENABLED) {
|
||
CONFIG.PHASE_DISPLAY_GAIN_DB = 0;
|
||
}
|
||
if ('phaseTrailEnabled' in payload) CONFIG.PHASE_TRAIL_ENABLED = !!payload.phaseTrailEnabled;
|
||
if ('classicNeedlesSlot' in payload) {
|
||
CONFIG.CLASSIC_NEEDLES_SLOT = normalizeMeterSlot(payload.classicNeedlesSlot, CONFIG_DEFAULTS.CLASSIC_NEEDLES_SLOT || 'vu');
|
||
}
|
||
if ('waveformColorLeft' in payload) {
|
||
CONFIG.WAVEFORM_COLOR_LEFT = normalizeColor(payload.waveformColorLeft, CONFIG_DEFAULTS.WAVEFORM_COLOR_LEFT || '#00e7ff');
|
||
}
|
||
if ('waveformColorRight' in payload) {
|
||
CONFIG.WAVEFORM_COLOR_RIGHT = normalizeColor(payload.waveformColorRight, CONFIG_DEFAULTS.WAVEFORM_COLOR_RIGHT || '#ff6b81');
|
||
}
|
||
if ('waveformColorDiff' in payload) {
|
||
CONFIG.WAVEFORM_COLOR_DIFF = normalizeColor(payload.waveformColorDiff, CONFIG_DEFAULTS.WAVEFORM_COLOR_DIFF || '#00e7ff');
|
||
}
|
||
if ('vuColorNormal' in payload) CONFIG.VU_COLOR_NORMAL = normalizeColor(payload.vuColorNormal, CONFIG_DEFAULTS.VU_COLOR_NORMAL || '#ffe066');
|
||
if ('vuColorWarn' in payload) CONFIG.VU_COLOR_WARN = normalizeColor(payload.vuColorWarn, CONFIG_DEFAULTS.VU_COLOR_WARN || '#ff3b3b');
|
||
if ('ppmDinColorNormal' in payload) CONFIG.PPM_DIN_COLOR_NORMAL = normalizeColor(payload.ppmDinColorNormal, CONFIG_DEFAULTS.PPM_DIN_COLOR_NORMAL || '#ffe066');
|
||
if ('ppmDinColorWarn' in payload) CONFIG.PPM_DIN_COLOR_WARN = normalizeColor(payload.ppmDinColorWarn, CONFIG_DEFAULTS.PPM_DIN_COLOR_WARN || '#ff3b3b');
|
||
if ('ppmEbuColorNormal' in payload) CONFIG.PPM_EBU_COLOR_NORMAL = normalizeColor(payload.ppmEbuColorNormal, CONFIG_DEFAULTS.PPM_EBU_COLOR_NORMAL || '#ffe066');
|
||
if ('ppmEbuColorWarn' in payload) CONFIG.PPM_EBU_COLOR_WARN = normalizeColor(payload.ppmEbuColorWarn, CONFIG_DEFAULTS.PPM_EBU_COLOR_WARN || '#ff3b3b');
|
||
if ('tpColorNormal' in payload) CONFIG.TP_COLOR_NORMAL = normalizeColor(payload.tpColorNormal, CONFIG_DEFAULTS.TP_COLOR_NORMAL || '#ffe066');
|
||
if ('tpColorWarn' in payload) CONFIG.TP_COLOR_WARN = normalizeColor(payload.tpColorWarn, CONFIG_DEFAULTS.TP_COLOR_WARN || '#ff3b3b');
|
||
if ('rmsColorNormal' in payload) CONFIG.RMS_COLOR_NORMAL = normalizeColor(payload.rmsColorNormal, CONFIG_DEFAULTS.RMS_COLOR_NORMAL || '#34d399');
|
||
if ('rmsColorWarn' in payload) CONFIG.RMS_COLOR_WARN = normalizeColor(payload.rmsColorWarn, CONFIG_DEFAULTS.RMS_COLOR_WARN || '#ff3b3b');
|
||
if ('lufsColorI' in payload) CONFIG.LUFS_COLOR_I = normalizeColor(payload.lufsColorI, CONFIG_DEFAULTS.LUFS_COLOR_I || '#34d399');
|
||
if ('lufsColorM' in payload) CONFIG.LUFS_COLOR_M = normalizeColor(payload.lufsColorM, CONFIG_DEFAULTS.LUFS_COLOR_M || '#ffe066');
|
||
if ('lufsColorS' in payload) CONFIG.LUFS_COLOR_S = normalizeColor(payload.lufsColorS, CONFIG_DEFAULTS.LUFS_COLOR_S || '#ff3b3b');
|
||
if ('lufsScaleLabelColor' in payload) {
|
||
CONFIG.LUFS_SCALE_LABEL_COLOR = normalizeColor(payload.lufsScaleLabelColor, CONFIG_DEFAULTS.LUFS_SCALE_LABEL_COLOR || '#8fd3d4');
|
||
}
|
||
if ('monoInput' in payload) CONFIG.MONO_INPUT = !!payload.monoInput;
|
||
if ('lrFractionalDelayEnabled' in payload) CONFIG.LR_FRACTIONAL_DELAY_ENABLED = !!payload.lrFractionalDelayEnabled;
|
||
const frac = Number(payload.lrFractionalDelaySamples);
|
||
if (Number.isFinite(frac)) CONFIG.LR_FRACTIONAL_DELAY_SAMPLES = Math.max(-1.5, Math.min(1.5, frac));
|
||
updateInputOffsetGain({
|
||
L: Number.isFinite(Number(payload.inputOffsetDbL)) ? Number(payload.inputOffsetDbL) : CONFIG.INPUT_OFFSET_DB_L,
|
||
R: Number.isFinite(Number(payload.inputOffsetDbR)) ? Number(payload.inputOffsetDbR) : CONFIG.INPUT_OFFSET_DB_R,
|
||
});
|
||
return buildPhoenixGlobalConfigPayload();
|
||
}
|
||
|
||
// Preset anwenden (wie Button „Apply"")
|
||
function applyBroadcastStandard(name) {
|
||
const p = BROADCAST_STANDARDS[name];
|
||
if (!p) return;
|
||
CONFIG.VU_DBFS_REF = p.VU_DBFS_REF;
|
||
if (typeof p.PPM_EBU_TOP === 'number') CONFIG.PPM_EBU_TOP = p.PPM_EBU_TOP;
|
||
if (typeof p.PPM_EBU_RED_START === 'number') CONFIG.PPM_EBU_RED_START = p.PPM_EBU_RED_START;
|
||
if (typeof p.PPM_DIN_TOP === 'number') CONFIG.PPM_DIN_TOP = p.PPM_DIN_TOP;
|
||
if (typeof p.PPM_DIN_RED_START === 'number') CONFIG.PPM_DIN_RED_START = p.PPM_DIN_RED_START;
|
||
CONFIG.TP_RED_START = p.TP_RED_START;
|
||
CONFIG.LUFS_RED_START = p.LUFS_RED_START;
|
||
CONFIG.LUFS_YELLOW_START = p.LUFS_YELLOW_START;
|
||
CONFIG.LUFS_GREEN_START = p.LUFS_GREEN_START;
|
||
CONFIG.VU_RED_START = p.VU_RED_START;
|
||
|
||
// Offsets zurücksetzen (wie bisher)
|
||
CONFIG.VU_OFFSET_DB = VU_OFFSET_ALIGNMENT_DB;
|
||
CONFIG.TP_OFFSET_DB = 0;
|
||
CONFIG.RMS_OFFSET_DB = 0;
|
||
|
||
saveConfig();
|
||
}
|
||
|
||
function resetConfigToDefaults() {
|
||
applySoftwarePreset('default', { persist: false });
|
||
}
|
||
|
||
// Export everything at the bottom
|
||
export {
|
||
CONFIG,
|
||
SOFTWARE_PRESET_IDS,
|
||
LAYOUT_PRESET_IDS,
|
||
PHOENIX_GLOBAL_OPTION_KEYS,
|
||
BROADCAST_STANDARDS,
|
||
loadConfig,
|
||
saveConfig,
|
||
buildPhoenixGlobalConfigPayload,
|
||
applyPhoenixGlobalConfig,
|
||
applyBroadcastStandard,
|
||
applySoftwarePreset,
|
||
loadSoftwarePreset,
|
||
saveSoftwarePreset,
|
||
loadLayoutPreset,
|
||
saveLayoutPreset,
|
||
exportSoftwarePreset,
|
||
importSoftwarePreset,
|
||
applyAlignmentProfile,
|
||
updateVuReference,
|
||
updateInputOffsetGain,
|
||
resetConfigToDefaults
|
||
|
||
};
|