Files
Phoenix/www/ui/options.js
T

1761 lines
66 KiB
JavaScript

// ui/options.js — bindet das Options-Panel an CONFIG (lesen/schreiben), Presets, Export/Import
// Erwartet vorhandenes DOM aus index.html. Nutzt localStorage-Key 'analyzer_config'.
import { CONFIG, saveConfig, loadConfig, applyAlignmentProfile, applyRtaBpoSelection, applySoftwarePreset, loadSoftwarePreset, saveSoftwarePreset, loadLayoutPreset, saveLayoutPreset, exportSoftwarePreset, importSoftwarePreset, updateVuReference, resetConfigToDefaults, updateInputOffsetGain } from '../core/config.js';
import { 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;
const trim = Number(CONFIG.PPM_DIN_TRIM_DB) || 0;
return baseMode + trim;
}
function dbfsToPpmDin(dbfs) {
const refDbfsFor0 = Number.isFinite(CONFIG.PPM_REF_DBFS_PEAK_FOR_0_DBU)
? CONFIG.PPM_REF_DBFS_PEAK_FOR_0_DBU
: -15;
return Number(dbfs) - refDbfsFor0 + getPpmDinVisibleBaseOffset();
}
function ppmDinToDbfs(ppmDin) {
const refDbfsFor0 = Number.isFinite(CONFIG.PPM_REF_DBFS_PEAK_FOR_0_DBU)
? CONFIG.PPM_REF_DBFS_PEAK_FOR_0_DBU
: -15;
return Number(ppmDin) + refDbfsFor0 - getPpmDinVisibleBaseOffset();
}
function normalizeNumericInput(value) {
return String(value ?? '')
.trim()
.replace(/[\u2212\u2012\u2013\u2014\u2015\uFE58\uFE63\uFF0D]/g, '-')
.replace(',', '.');
}
function formatThresholdValue(value) {
return String(Math.round(Number(value) * 10) / 10);
}
function parseNumericInput(value, fallback) {
const normalized = normalizeNumericInput(value);
const match = normalized.match(/[-+]?(?:\d+\.?\d*|\.\d+)/);
const n = Number(match ? match[0] : normalized);
return Number.isFinite(n) ? n : fallback;
}
function clampThresholdDbfs(value, fallback) {
const n = parseNumericInput(value, fallback);
return clamp(Number.isFinite(n) ? n : fallback, -120, 20);
}
function syncLinkedThresholdFields(dbfsId, ppmId, dbfsValue) {
const dbfsEl = E(dbfsId);
const ppmEl = E(ppmId);
if (dbfsEl) dbfsEl.value = formatThresholdValue(dbfsValue);
if (ppmEl) ppmEl.value = formatThresholdValue(dbfsToPpmDin(dbfsValue));
}
function showPpmLoudnessWarning(onConfirm) {
const wrap = E('ppmLoudnessWarn');
const ack = E('ppmLoudnessWarnAck');
const btn = E('ppmLoudnessWarnBtn');
if (!wrap || !ack || !btn) {
if (onConfirm) onConfirm();
return;
}
if (!wrap.dataset.bound) {
btn.addEventListener('click', () => {
if (ack.checked) {
try { localStorage.setItem(PPM_LOUD_WARN_KEY, '1'); } catch (_) {}
}
wrap.style.display = 'none';
if (onConfirm) onConfirm();
});
wrap.dataset.bound = '1';
}
ack.checked = false;
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();
enforcePhaseAmplitudeConstraints({ persist: true });
syncUI();
wireHandlers(env);
bindOptionsViewportAssist();
bindAlignmentToggle(env);
bindVuRefToggle(env);
// Sichtbarkeit nach View steuern übernimmt main.js — hier nur Export der Sync-Funktion
return { syncUI };
}
let optionsViewportAssistBound = false;
function bindOptionsViewportAssist() {
if (optionsViewportAssistBound) return;
optionsViewportAssistBound = true;
const panel = E('optionsPanelNew');
if (!panel) return;
const setKeyboardInset = () => {
const vv = window.visualViewport;
if (!vv) {
panel.style.setProperty('--options-kb-inset', '0px');
return;
}
const inset = Math.max(0, window.innerHeight - (vv.height + vv.offsetTop));
panel.style.setProperty('--options-kb-inset', `${Math.round(inset)}px`);
};
const revealFocusedField = (target) => {
if (!panel || !target || !panel.contains(target)) return;
const vv = window.visualViewport;
const panelRect = panel.getBoundingClientRect();
const targetRect = target.getBoundingClientRect();
const keyboardInset = vv ? Math.max(0, window.innerHeight - (vv.height + vv.offsetTop)) : 0;
const visibleTop = panelRect.top + 16;
const visibleBottom = panelRect.bottom - keyboardInset - 16;
if (targetRect.bottom > visibleBottom || targetRect.top < visibleTop) {
const targetCenter = targetRect.top + (targetRect.height / 2);
const visibleCenter = visibleTop + ((visibleBottom - visibleTop) / 2);
const delta = targetCenter - visibleCenter;
panel.scrollTop += delta;
}
};
const onFocus = (ev) => {
const el = ev.target;
if (!(el instanceof HTMLElement)) return;
const type = String(el.getAttribute('type') || '').toLowerCase();
if (el.tagName !== 'INPUT' && el.tagName !== 'SELECT' && el.tagName !== 'TEXTAREA') return;
if (type === 'checkbox' || type === 'range' || type === 'color') return;
setKeyboardInset();
requestAnimationFrame(() => revealFocusedField(el));
setTimeout(() => revealFocusedField(el), 250);
};
const onBlur = () => {
setTimeout(() => {
const active = document.activeElement;
if (!(active instanceof HTMLElement) || !panel.contains(active)) {
panel.style.setProperty('--options-kb-inset', '0px');
}
}, 50);
};
panel.addEventListener('focusin', onFocus);
panel.addEventListener('focusout', onBlur);
if (window.visualViewport) {
window.visualViewport.addEventListener('resize', () => {
setKeyboardInset();
const active = document.activeElement;
if (active instanceof HTMLElement && panel.contains(active)) {
requestAnimationFrame(() => revealFocusedField(active));
}
});
window.visualViewport.addEventListener('scroll', setKeyboardInset);
}
setKeyboardInset();
}
function syncSoftwarePresetControls() {
const presetId = String(E('opt_softwarePreset')?.value || CONFIG.SOFTWARE_PRESET || 'default').toLowerCase();
const isDefault = presetId === 'default';
const btnSave = E('btnSoftwarePresetSave');
const btnImport = E('btnSoftwarePresetImport');
const inputImport = E('impSoftwarePreset');
if (btnSave) {
btnSave.disabled = isDefault;
btnSave.title = isDefault ? 'Für Default nicht verfügbar' : '';
}
if (inputImport) inputImport.disabled = isDefault;
if (btnImport) {
btnImport.setAttribute('aria-disabled', isDefault ? 'true' : 'false');
btnImport.style.opacity = isDefault ? '0.55' : '';
btnImport.style.pointerEvents = isDefault ? 'none' : '';
btnImport.style.cursor = isDefault ? 'not-allowed' : 'pointer';
btnImport.title = isDefault ? 'Für Default nicht verfügbar' : '';
}
}
function syncUI() {
const map = [
['opt_softwarePreset', CONFIG.SOFTWARE_PRESET || 'default'],
['opt_fft', String(CONFIG.FFT_SIZE)],
['opt_phoenixBaseUrl', CONFIG.PHOENIX_BASE_URL || `http://${location.hostname || '127.0.0.1'}:8789`],
['opt_alMarkers', CONFIG.AL_MARKERS_ENABLED, null, null, 'checkbox'],
['opt_inputOffsetDbL', CONFIG.INPUT_OFFSET_DB_L, 'val_inputOffsetDbL', (v)=>`${Number(v).toFixed(1)} dB`],
['opt_inputOffsetDbR', CONFIG.INPUT_OFFSET_DB_R, 'val_inputOffsetDbR', (v)=>`${Number(v).toFixed(1)} dB`],
['opt_monoInput', CONFIG.MONO_INPUT, null, null, 'checkbox'],
['opt_lrFracDelayEnabled', CONFIG.LR_FRACTIONAL_DELAY_ENABLED, null, null, 'checkbox'],
['opt_lrFracDelaySamples', CONFIG.LR_FRACTIONAL_DELAY_SAMPLES ?? 0.05, 'val_lrFracDelaySamples', (v)=>`${Number(v).toFixed(3)} Samples`],
['opt_screensaverEnabled', CONFIG.SCREENSAVER_ENABLED !== false, null, null, 'checkbox'],
['opt_screensaverIdleMin', CONFIG.SCREENSAVER_IDLE_MIN ?? 5, 'val_screensaverIdleMin', (v)=>`${Number(v).toFixed(1)} min`],
['opt_screensaverActivityDb', CONFIG.SCREENSAVER_ACTIVITY_DB ?? -50, 'val_screensaverActivityDb', (v)=>`${Number(v).toFixed(1)} dBFS`],
['opt_screensaverMode', CONFIG.SCREENSAVER_MODE || 'clock'],
['opt_screensaverGlow', CONFIG.SCREENSAVER_LED_GLOW !== false, null, null, 'checkbox'],
['opt_screensaverColor', CONFIG.SCREENSAVER_LED_COLOR || '#ff0000'],
['opt_clockGlow', CONFIG.CLOCK_LED_GLOW !== false, null, null, 'checkbox'],
['opt_clockStyle', CONFIG.CLOCK_STYLE || 'analog'],
['opt_clockColor', CONFIG.CLOCK_LED_COLOR || '#ff0000'],
['opt_clockPpmLed', CONFIG.CLOCK_PPM_LED_MODE === true, null, null, 'checkbox'],
['opt_vuRef', CONFIG.VU_DBFS_REF],
['opt_vuOff', CONFIG.VU_OFFSET_DB],
['opt_vuHeaderValue', CONFIG.VU_HEADER_SHOW_VALUE, null, null, 'checkbox'],
['opt_ppmDinColNorm', CONFIG.PPM_DIN_COLOR_NORMAL],
['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],
['opt_ppmEbuHeaderValue', CONFIG.PPM_EBU_HEADER_SHOW_VALUE, null, null, 'checkbox'],
['opt_hifiAlign', CONFIG.HIFI_PEAK_ALIGNMENT || 'ppm_din_minus5'],
['opt_tpOff', CONFIG.TP_OFFSET_DB],
['opt_tpHeaderValue', CONFIG.TP_HEADER_SHOW_VALUE, null, null, 'checkbox'],
['opt_rmsOff', CONFIG.RMS_OFFSET_DB],
['opt_rmsHeaderValue', CONFIG.RMS_HEADER_SHOW_VALUE, null, null, 'checkbox'],
['opt_rmsTc', CONFIG.RMS_TC_MODE || 'fast'],
// NEW: RMS Modus & Referenzen
['opt_rmsMode', (CONFIG.RMS_MODE ?? 'dbu')], // 'dbu' | 'dbfs'
['opt_rmsRefDbfs', CONFIG.RMS_REF_DBFS_FOR_REF_DBU ?? -18],
['opt_rmsRefDbu', CONFIG.RMS_REF_DBU ?? +4],
['opt_vuRedThr', CONFIG.VU_RED_START],
['opt_tpRedThr', CONFIG.TP_RED_START],
['opt_rmsRedThr', CONFIG.RMS_RED_START],
['opt_vuRedBarOnly', CONFIG.VU_RED_BAR_ONLY, null, null, 'checkbox'],
['opt_tpRedBarOnly', CONFIG.TP_RED_BAR_ONLY, null, null, 'checkbox'],
['opt_rmsRedBarOnly', CONFIG.RMS_RED_BAR_ONLY, null, null, 'checkbox'],
['opt_vuColNorm', CONFIG.VU_COLOR_NORMAL],
['opt_vuColWarn', CONFIG.VU_COLOR_WARN],
['opt_tpColNorm', CONFIG.TP_COLOR_NORMAL],
['opt_tpColWarn', CONFIG.TP_COLOR_WARN],
['opt_rmsColNorm', CONFIG.RMS_COLOR_NORMAL],
['opt_rmsColWarn', CONFIG.RMS_COLOR_WARN],
['opt_corrResponse', String(Number(CONFIG.CORR_RESPONSE_S) >= 1.75 ? 2.5 : 1)],
['opt_corrNegativeMarkerMode', CONFIG.CORR_NEGATIVE_MARKER_MODE || 'memory'],
['opt_xyPoints', String(CONFIG.XY_POINTS)],
['opt_xyStyle', CONFIG.XY_STYLE],
['opt_xySilenceGate', CONFIG.XY_SILENCE_GATE_ENABLED, null, null, 'checkbox'],
['opt_xySilenceThr', CONFIG.XY_SILENCE_THRESHOLD_RMS_DBFS ?? -75],
['opt_barThin', CONFIG.METER_BAR_THIN, 'val_barThin', (v)=>Number(v).toFixed(2)],
['opt_headerTextColor', CONFIG.HEADER_TEXT_COLOR || '#ffe066'],
['opt_rtaEngine', CONFIG.RTA_ENGINE || 'iir'],
['opt_rtaFilterbank', CONFIG.RTA_IIR_FILTERBANK || 'legacy'],
['opt_rtaBpo', CONFIG.RTA_BPO_MODE || '1_6'],
['opt_rtaOrder', CONFIG.RTA_IIR_ORDER ?? 4],
['opt_rtaDisplayGainFft', CONFIG.RTA_DISPLAY_GAIN_FFT_DB ?? 12, 'val_rtaDisplayGainFft', (v)=>`${v} dB`],
['opt_rtaDisplayGainIir', CONFIG.RTA_DISPLAY_GAIN_IIR_DB ?? -6, 'val_rtaDisplayGainIir', (v)=>`${v} dB`],
['opt_rtaFreq', CONFIG.RTA_FREQ_RANGE || 'norm'],
['opt_rtaLayout', CONFIG.RTA_BAR_LAYOUT || 'iec'],
['opt_rtaBarBaseColor', CONFIG.RTA_BAR_BASE_COLOR || '#ffe066'],
['opt_rtaWeighting', CONFIG.RTA_WEIGHTING || 'z'],
['opt_rtaDetector', CONFIG.RTA_DETECTOR || 'average'],
['opt_rtaIntegration', CONFIG.RTA_INTEGRATION || 'fast'],
['opt_rtaBallistics', CONFIG.RTA_BALLISTICS_MODE || 'average'],
['opt_rtaHoldMode', CONFIG.RTA_PEAK_HOLD_MODE || 'auto'],
['opt_rtaHoldTime', CONFIG.RTA_PEAK_HOLD_SEC ?? 2.5],
['opt_rtaDisplayHold', CONFIG.RTA_DISPLAY_HOLD_SEC ?? 0],
['opt_spectroGamma', CONFIG.SPECTRO_GAMMA ?? 0.9, 'val_spectroGamma', (v)=>Number(v).toFixed(2)],
['opt_spectroScroll', CONFIG.SPECTRO_SCROLL_MODE ?? 1],
['opt_peakHistScroll', CONFIG.PEAK_HISTORY_SCROLL_MODE ?? 1],
['opt_peakHistFill', CONFIG.PEAK_HISTORY_FILL_ENABLED, null, null, 'checkbox'],
['opt_peakHistFillInvert', CONFIG.PEAK_HISTORY_FILL_INVERT, null, null, 'checkbox'],
['opt_waveMode', CONFIG.WAVEFORM_MODE || 'stacked'],
['opt_waveWindow', CONFIG.WAVEFORM_WINDOW_SEC || 1],
['opt_waveColorL', CONFIG.WAVEFORM_COLOR_LEFT || '#00e7ff'],
['opt_waveColorR', CONFIG.WAVEFORM_COLOR_RIGHT || '#ff6b81'],
['opt_waveColorDiff', CONFIG.WAVEFORM_COLOR_DIFF || '#00e7ff'],
['opt_rtaTauFast', CONFIG.RTA_IIR_TAU_FAST ?? 0.125],
['opt_rtaTauSlow', CONFIG.RTA_IIR_TAU_SLOW ?? 1.0],
['opt_rtRenderMode', CONFIG.REALTIME_RENDER_STYLE || 'bars'],
['opt_rtHold', CONFIG.REALTIME_BAR_HOLD_MS],
['opt_rtDecay', CONFIG.REALTIME_BAR_DECAY_DB_PER_S],
['opt_goniGain', CONFIG.GONIO_DISPLAY_GAIN_DB, 'val_goniGain', (v)=>`${Number(v).toFixed(0)} dB`],
['opt_goniAgc', CONFIG.GONIO_AGC_ENABLED, null, null, 'checkbox'],
['opt_phaseGain', CONFIG.PHASE_DISPLAY_GAIN_DB ?? 0, 'val_phaseGain', (v)=>`${Number(v).toFixed(0)} dB`],
['opt_phaseAgc', CONFIG.PHASE_AGC_ENABLED, null, null, 'checkbox'],
['opt_phaseAmpMode', CONFIG.PHASE_AMPLITUDE_MODE || 'bandpass'],
['opt_phaseTrail', CONFIG.PHASE_TRAIL_ENABLED, null, null, 'checkbox'],
['opt_goniGap', CONFIG.GONI_METER_GAP, 'val_goniGap', (v)=>`${v} px`],
['opt_goniFade', CONFIG.GONIO_LINE_FADE_MS, 'val_goniFade', (v)=>`${Math.round(v)} ms`],
['opt_goniPersistence', CONFIG.GONIO_PERSISTENCE_MODE || 'fast'],
['opt_panelDividers', CONFIG.PANEL_DIVIDERS_ENABLED, null, null, 'checkbox'],
['opt_lufsRedThr', CONFIG.LUFS_RED_START],
['opt_lufsYellowThr', CONFIG.LUFS_YELLOW_START],
['opt_lufsGreenThr', CONFIG.LUFS_GREEN_START],
['opt_lufsColI', CONFIG.LUFS_COLOR_I || CONFIG.LUFS_COLOR_I_GREEN || CONFIG.LUFS_COLOR_GREEN],
['opt_lufsColM', CONFIG.LUFS_COLOR_M || CONFIG.LUFS_COLOR_M_GREEN || CONFIG.LUFS_COLOR_GREEN],
['opt_lufsColS', CONFIG.LUFS_COLOR_S || CONFIG.LUFS_COLOR_S_GREEN || CONFIG.LUFS_COLOR_GREEN],
['opt_lufsScaleCol', CONFIG.LUFS_SCALE_LABEL_COLOR || '#8fd3d4'],
['opt_lufsIWindowMin', CONFIG.LUFS_I_WINDOW_MIN ?? 4, 'val_lufsIWindowMin', (v)=>`${Number(v).toFixed(0)} min`],
['opt_lufsINorm', CONFIG.LUFS_I_NORM_ENABLED, null, null, 'checkbox'],
['opt_stopwatchDisplay', CONFIG.STOPWATCH_DISPLAY_STYLE || 'mono'],
['opt_recFormat', CONFIG.RECORD_OUTPUT_FORMAT || 'wav'],
['opt_recMp3Bitrate', String(CONFIG.RECORD_MP3_BITRATE_KBPS || 192)],
['recTargetSel', CONFIG.RECORD_TARGET || 'download'],
['opt_recAutoDownload', CONFIG.RECORD_AUTO_DOWNLOAD === true, null, null, 'checkbox'],
['opt_recShowAb', CONFIG.RECORD_SHOW_RECORDER_AB === true, null, null, 'checkbox'],
['opt_recAutoGap', CONFIG.RECORD_AUTO_SPLIT_GAP_SEC ?? 1.5, 'val_recAutoGap', (v)=>`${Number(v).toFixed(1)} s`],
['opt_splitTapAction', CONFIG.SPLIT_VIEW_TAP_ACTION || 'switch'],
['opt_splitLeftPlot', CONFIG.SPLIT_VIEW_LEFT || 'phase-wheel'],
['opt_splitRightPlot', CONFIG.SPLIT_VIEW_RIGHT || 'realtime'],
['opt_quadTapAction', CONFIG.QUAD_VIEW_TAP_ACTION || 'switch'],
['opt_quadPlotTL', CONFIG.QUAD_VIEW_TL || 'phase-wheel'],
['opt_quadPlotTR', CONFIG.QUAD_VIEW_TR || 'realtime'],
['opt_quadPlotBL', CONFIG.QUAD_VIEW_BL || 'goniometer-rtw'],
['opt_quadPlotBR', CONFIG.QUAD_VIEW_BR || 'none'],
['opt_quadMeterCount', CONFIG.QUAD_VIEW_METER_COUNT ?? 0],
['opt_quadMeter1', CONFIG.QUAD_VIEW_METER_1 || 'vu'],
['opt_quadMeter1Pos', CONFIG.QUAD_VIEW_METER_1_POS || 'right'],
['opt_quadMeter2', CONFIG.QUAD_VIEW_METER_2 || 'ppm-din'],
['opt_quadMeter2Pos', CONFIG.QUAD_VIEW_METER_2_POS || 'right'],
['opt_quadMeter3', CONFIG.QUAD_VIEW_METER_3 || 'lufs'],
['opt_quadMeter3Pos', CONFIG.QUAD_VIEW_METER_3_POS || 'right'],
['opt_splitMeterCount', CONFIG.SPLIT_VIEW_METER_COUNT ?? 0],
['opt_splitMeter1', CONFIG.SPLIT_VIEW_METER_1 || 'vu'],
['opt_splitMeter1Pos', CONFIG.SPLIT_VIEW_METER_1_POS || 'right'],
['opt_splitMeter2', CONFIG.SPLIT_VIEW_METER_2 || 'ppm-din'],
['opt_splitMeter2Pos', CONFIG.SPLIT_VIEW_METER_2_POS || 'right'],
['opt_splitMeter3', CONFIG.SPLIT_VIEW_METER_3 || 'lufs'],
['opt_splitMeter3Pos', CONFIG.SPLIT_VIEW_METER_3_POS || 'right'],
// Hold and decay settings for Bars and VU
['opt_vuHold', CONFIG.VU_HOLD_MS],
['opt_vuDecay', CONFIG.VU_DECAY_DB_PER_S],
['opt_tpHold', CONFIG.TP_HOLD_MS],
['opt_tpDecay', CONFIG.TP_DECAY_DB_PER_S],
];
for (const [id,val,disp,fmt,typ] of map) {
const el = E(id);
if (!el) continue;
if (typ === 'checkbox') {
el.checked = !!val;
} else {
el.value = val;
}
if (disp) {
const d = E(disp);
if (d) d.textContent = fmt ? fmt(val) : val;
}
}
syncLinkedThresholdFields('opt_stopwatchThreshDbfs', 'opt_stopwatchThreshPpmDin', clampThresholdDbfs(CONFIG.STOPWATCH_AUTO_THRESHOLD_DBFS, -70));
syncSoftwarePresetControls();
syncLinkedThresholdFields('opt_recThreshDbfs', 'opt_recThreshPpmDin', clampThresholdDbfs(CONFIG.RECORD_AUTO_THRESHOLD_DBFS, -50));
const gainSliderInit = E('opt_goniGain');
if (gainSliderInit) {
gainSliderInit.disabled = !!CONFIG.GONIO_AGC_ENABLED;
const initialManual = Number.isFinite(CONFIG.GONIO_MANUAL_GAIN_DB)
? CONFIG.GONIO_MANUAL_GAIN_DB
: (Number.isFinite(CONFIG.GONIO_DISPLAY_GAIN_DB) ? CONFIG.GONIO_DISPLAY_GAIN_DB : 0);
gainSliderInit.dataset.lastManualGain = String(initialManual);
}
const phaseGainSliderInit = E('opt_phaseGain');
if (phaseGainSliderInit) {
phaseGainSliderInit.disabled = !!CONFIG.PHASE_AGC_ENABLED;
}
enforcePhaseAmplitudeConstraints({ persist: false });
const dinToggle = E('opt_din_al_minus6');
if (dinToggle) {
dinToggle.checked = CONFIG.PPM_DIN_MODE !== 'al_minus9';
}
const vuToggle = E('opt_vu_plus4');
if (vuToggle) {
vuToggle.checked = CONFIG.VU_REF_IS_PLUS4 !== false;
}
applyScreensaverDisabled();
applyClockPpmDisabled();
const rtwOff = E('opt_ppmDinLoudOff');
if (rtwOff) rtwOff.disabled = !CONFIG.PPM_DIN_LOUDNESS_BOXES;
const iWin = E('opt_lufsIWindowMin');
if (iWin) iWin.disabled = !!CONFIG.LUFS_I_NORM_ENABLED;
const rtaResponse = E('opt_rtaIntegration');
if (rtaResponse) rtaResponse.disabled = CONFIG.RTA_DETECTOR === 'peak';
const rtaTauFast = E('opt_rtaTauFast');
if (rtaTauFast) rtaTauFast.disabled = true;
const rtaTauSlow = E('opt_rtaTauSlow');
if (rtaTauSlow) rtaTauSlow.disabled = true;
const mp3Wrap = E('opt_recMp3BitrateWrap');
if (mp3Wrap) mp3Wrap.style.display = (String(CONFIG.RECORD_OUTPUT_FORMAT || 'wav') === 'mp3') ? '' : 'none';
const mp3Sel = E('opt_recMp3Bitrate');
if (mp3Sel) mp3Sel.disabled = String(CONFIG.RECORD_OUTPUT_FORMAT || 'wav') !== 'mp3';
applySplitViewOptionsDisabled();
applyQuadViewOptionsDisabled();
}
function applyScreensaverDisabled() {
const enabled = CONFIG.SCREENSAVER_ENABLED !== false;
const idle = E('opt_screensaverIdleMin');
const idleLabel = E('val_screensaverIdleMin');
const thr = E('opt_screensaverActivityDb');
const thrLabel = E('val_screensaverActivityDb');
const btn = E('opt_screensaverTest');
const modeSel = E('opt_screensaverMode');
const glow = E('opt_screensaverGlow');
const color = E('opt_screensaverColor');
[idle, thr, btn, modeSel, glow, color].forEach((el) => {
if (el) el.disabled = !enabled;
});
if (!enabled) {
if (idleLabel) idleLabel.textContent = '';
if (thrLabel) thrLabel.textContent = '';
}
}
function applyClockPpmDisabled() {
const ledOpt = E('opt_clockPpmLed');
if (!ledOpt) return;
const isPpmStyle = CONFIG.CLOCK_STYLE === 'analog-ppm' || CONFIG.CLOCK_STYLE === 'digital-ppm';
ledOpt.disabled = !isPpmStyle;
ledOpt.parentElement?.classList?.toggle('disabled', !isPpmStyle);
}
function applySplitViewOptionsDisabled() {
const clampCount = (v) => {
const n = Number(v);
if (!Number.isFinite(n)) return 0;
return Math.max(0, Math.min(3, n | 0));
};
const count = clampCount(CONFIG.SPLIT_VIEW_METER_COUNT);
const m1 = E('opt_splitMeter1');
const m1p = E('opt_splitMeter1Pos');
const m2 = E('opt_splitMeter2');
const m2p = E('opt_splitMeter2Pos');
const m3 = E('opt_splitMeter3');
const m3p = E('opt_splitMeter3Pos');
if (m1) m1.disabled = count < 1;
if (m1p) m1p.disabled = count < 1;
if (m2) m2.disabled = count < 2;
if (m2p) m2p.disabled = count < 2;
if (m3) m3.disabled = count < 3;
if (m3p) m3p.disabled = count < 3;
}
function applyQuadViewOptionsDisabled() {
const clampCount = (v) => {
const n = Number(v);
if (!Number.isFinite(n)) return 0;
return Math.max(0, Math.min(3, n | 0));
};
const count = clampCount(CONFIG.QUAD_VIEW_METER_COUNT);
const m1 = E('opt_quadMeter1');
const m1p = E('opt_quadMeter1Pos');
const m2 = E('opt_quadMeter2');
const m2p = E('opt_quadMeter2Pos');
const m3 = E('opt_quadMeter3');
const m3p = E('opt_quadMeter3Pos');
if (m1) m1.disabled = count < 1;
if (m1p) m1p.disabled = count < 1;
if (m2) m2.disabled = count < 2;
if (m2p) m2p.disabled = count < 2;
if (m3) m3.disabled = count < 3;
if (m3p) m3p.disabled = count < 3;
}
function enforcePhaseAmplitudeConstraints({ persist = false } = {}) {
const isPpmDin = CONFIG.PHASE_AMPLITUDE_MODE === 'ppm-din';
const prevAgc = !!CONFIG.PHASE_AGC_ENABLED;
if (isPpmDin && prevAgc) {
CONFIG.PHASE_AGC_ENABLED = false;
}
const agcEl = E('opt_phaseAgc');
if (agcEl) {
agcEl.disabled = isPpmDin;
agcEl.checked = isPpmDin ? false : !!CONFIG.PHASE_AGC_ENABLED;
agcEl.title = isPpmDin ? 'Nicht verfügbar bei PPM DIN Amplitude' : '';
}
const gainSliderEl = E('opt_phaseGain');
if (gainSliderEl) {
gainSliderEl.disabled = !!CONFIG.PHASE_AGC_ENABLED;
}
if (persist && prevAgc !== !!CONFIG.PHASE_AGC_ENABLED) {
saveConfig();
}
}
function wireHandlers(env) {
const reloadAudioIfNeeded = (force = false) => {
if (force || env?.audioReload) {
try { env?.audioReload?.(); } catch (_) {}
}
};
const notifyPhoenixGlobalConfig = () => {
try { env?.notifyPhoenixGlobalConfig?.(); } catch (_) {}
};
const h = (id, update, dispId, fmt, isCheckbox=false) => {
const el = E(id);
if (!el) return;
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';
const commit = () => {
const raw = isCheckbox ? el.checked : el.value;
const ret = update(raw, el);
if (!isCheckbox && ret !== undefined) {
el.value = ret;
}
saveConfig();
if (notifyRta) {
try { env?.notifyRtaConfig?.(); } catch (_) {}
}
if (notifyPpm) {
try { env?.notifyPpmConfig?.(); } catch (_) {}
}
try { env?.syncRealtimeGain?.(); } catch (_) {}
try { env?.syncWaveformWindow?.(); } catch (_) {}
try { env?.syncWaveformMode?.(); } catch (_) {}
try { env?.syncRtaBpo?.(); } catch (_) {}
try { env?.syncSplitView?.(); } catch (_) {}
if (dispId) {
const d = E(dispId);
if (d) {
const showVal = (ret !== undefined) ? ret : raw;
d.textContent = fmt ? fmt(showVal) : showVal;
}
}
};
el.oninput = commitOnInput ? commit : null;
el.onchange = commit;
if (!isCheckbox && !commitOnInput) {
el.onkeydown = (ev) => {
if (ev.key === 'Enter') {
ev.preventDefault();
el.blur();
}
};
}
};
const wireLinkedThresholdPair = (dbfsId, ppmId, getValue, setValue, fallback, afterCommit = null) => {
const dbfsEl = E(dbfsId);
const ppmEl = E(ppmId);
if (!dbfsEl || !ppmEl) return;
const applyDbfs = (dbfs) => {
const next = clampThresholdDbfs(dbfs, fallback);
setValue(next);
saveConfig();
try { afterCommit?.(next); } catch (_) {}
syncLinkedThresholdFields(dbfsId, ppmId, next);
};
const commitDbfs = () => applyDbfs(dbfsEl.value);
const commitPpm = () => applyDbfs(ppmDinToDbfs(ppmEl.value));
dbfsEl.onchange = commitDbfs;
ppmEl.onchange = commitPpm;
dbfsEl.onkeydown = (ev) => {
if (ev.key === 'Enter') {
ev.preventDefault();
dbfsEl.blur();
}
};
ppmEl.onkeydown = (ev) => {
if (ev.key === 'Enter') {
ev.preventDefault();
ppmEl.blur();
}
};
dbfsEl.onblur = commitDbfs;
ppmEl.onblur = commitPpm;
syncLinkedThresholdFields(dbfsId, ppmId, clampThresholdDbfs(getValue(), fallback));
};
h('opt_fft', v => {
CONFIG.FFT_SIZE = clampPow2(+v, 2048, 16384);
notifyPhoenixGlobalConfig();
return CONFIG.FFT_SIZE;
});
h('opt_phoenixBaseUrl', v => {
CONFIG.PHOENIX_BASE_URL = String(v || '').trim() || `http://${location.hostname || '127.0.0.1'}:8789`;
reloadAudioIfNeeded(false);
return CONFIG.PHOENIX_BASE_URL;
});
h('opt_alMarkers', v => {
CONFIG.AL_MARKERS_ENABLED = !!v;
}, null, null, true);
const setInputOffsetDb = (side, rawValue) => {
const raw = Number(rawValue);
const snapped = Math.round(raw * 10) / 10; // 0.1 dB Schritte
let val = Number.isFinite(snapped) ? snapped : -4;
val = clamp(val, -10, 10);
if (side === 'L') {
CONFIG.INPUT_OFFSET_DB_L = val;
} else {
CONFIG.INPUT_OFFSET_DB_R = val;
}
updateInputOffsetGain({
L: CONFIG.INPUT_OFFSET_DB_L,
R: CONFIG.INPUT_OFFSET_DB_R,
});
try { env?.updateInputOffset?.(); } catch (_) {}
return val;
};
h('opt_inputOffsetDbL', v => setInputOffsetDb('L', v), 'val_inputOffsetDbL', (v)=>`${Number(v).toFixed(1)} dB`);
h('opt_inputOffsetDbR', v => setInputOffsetDb('R', v), 'val_inputOffsetDbR', (v)=>`${Number(v).toFixed(1)} dB`);
h('opt_monoInput', v => {
CONFIG.MONO_INPUT = !!v;
try { env?.updateMonoInput?.(); } catch (_) {}
}, null, null, true);
h('opt_lrFracDelayEnabled', v => {
CONFIG.LR_FRACTIONAL_DELAY_ENABLED = !!v;
try { env?.updateLrDelay?.(); } catch (_) {}
}, null, null, true);
h('opt_screensaverEnabled', v => {
const enabled = !!v;
CONFIG.SCREENSAVER_ENABLED = enabled;
try { env?.screensaver?.setEnabled?.(enabled); } catch (_) {}
notifyPhoenixGlobalConfig();
applyScreensaverDisabled();
}, null, null, true);
h('opt_screensaverMode', v => {
const mode = v === 'starfield'
? 'starfield'
: (v === 'clock'
? 'clock'
: (v === 'black'
? 'black'
: 'dvd'));
CONFIG.SCREENSAVER_MODE = mode;
try { env?.screensaver?.setMode?.(mode); } catch (_) {}
return mode;
});
h('opt_screensaverGlow', v => {
const enabled = !!v;
CONFIG.SCREENSAVER_LED_GLOW = enabled;
notifyPhoenixGlobalConfig();
return enabled;
}, null, null, true);
h('opt_screensaverColor', v => {
const val = String(v || '#ff0000').trim() || '#ff0000';
CONFIG.SCREENSAVER_LED_COLOR = val;
try { env?.screensaver?.setColor?.(val); } catch (_) {}
notifyPhoenixGlobalConfig();
return val;
});
h('opt_clockGlow', v => {
const enabled = !!v;
CONFIG.CLOCK_LED_GLOW = enabled;
return enabled;
}, null, null, true);
h('opt_clockStyle', v => {
const style = (v === 'digital')
? 'digital'
: (v === 'analog-ppm')
? 'analog-ppm'
: (v === 'digital-ppm')
? 'digital-ppm'
: 'analog';
CONFIG.CLOCK_STYLE = style;
applyClockPpmDisabled();
return style;
});
h('opt_clockPpmLed', v => {
CONFIG.CLOCK_PPM_LED_MODE = !!v;
return CONFIG.CLOCK_PPM_LED_MODE;
}, null, null, true);
h('opt_clockColor', v => {
const val = v || '#ff0000';
CONFIG.CLOCK_LED_COLOR = val;
notifyPhoenixGlobalConfig();
return val;
});
h('opt_lrFracDelaySamples', v => {
let val = parseNumericInput(v, Number(CONFIG.LR_FRACTIONAL_DELAY_SAMPLES));
if (!Number.isFinite(val)) val = Number(CONFIG.LR_FRACTIONAL_DELAY_SAMPLES);
if (!Number.isFinite(val)) val = 0;
val = clamp(val, -1.5, 1.5);
CONFIG.LR_FRACTIONAL_DELAY_SAMPLES = val;
try { env?.updateLrDelay?.(); } catch (_) {}
return val;
}, 'val_lrFracDelaySamples', (v)=>`${Number(v).toFixed(3)} Samples`);
h('opt_screensaverIdleMin', v => {
if (CONFIG.SCREENSAVER_ENABLED === false) return CONFIG.SCREENSAVER_IDLE_MIN;
let val = Number(v);
if (!Number.isFinite(val)) val = CONFIG.SCREENSAVER_IDLE_MIN ?? 5;
val = clamp(val, 0, 120);
CONFIG.SCREENSAVER_IDLE_MIN = val;
try { env?.screensaver?.setIdleMinutes?.(val); } catch (_) {}
notifyPhoenixGlobalConfig();
return val;
}, 'val_screensaverIdleMin', (v)=>`${Number(v).toFixed(1)} min`);
h('opt_screensaverActivityDb', v => {
if (CONFIG.SCREENSAVER_ENABLED === false) return CONFIG.SCREENSAVER_ACTIVITY_DB;
let val = Number(v);
if (!Number.isFinite(val)) val = CONFIG.SCREENSAVER_ACTIVITY_DB ?? -50;
val = clamp(val, -120, 0);
CONFIG.SCREENSAVER_ACTIVITY_DB = val;
try { env?.screensaver?.setActivityThreshold?.(val); } catch (_) {}
notifyPhoenixGlobalConfig();
return val;
}, 'val_screensaverActivityDb', (v)=>`${Number(v).toFixed(1)} dBFS`);
const ssTest = E('opt_screensaverTest');
if (ssTest) {
ssTest.onclick = (e) => {
if (e && typeof e.stopPropagation === 'function') e.stopPropagation();
if (e && typeof e.preventDefault === 'function') e.preventDefault();
try { env?.screensaver?.triggerTest?.(); } catch (_) {}
};
}
const resetSlotsBtn = E('opt_resetSlots');
if (resetSlotsBtn && !resetSlotsBtn.dataset.bound) {
resetSlotsBtn.addEventListener('click', (e) => {
if (e && typeof e.stopPropagation === 'function') e.stopPropagation();
if (e && typeof e.preventDefault === 'function') e.preventDefault();
try { env?.resetSlotsToDefaults?.(); } catch (_) {}
});
resetSlotsBtn.dataset.bound = '1';
}
h('opt_vuRef', v => CONFIG.VU_DBFS_REF = clamp(+v, -30, -6));
h('opt_vuOff', v => CONFIG.VU_OFFSET_DB = clamp(+v, -20, +20));
h('opt_vuHeaderValue', v => {
CONFIG.VU_HEADER_SHOW_VALUE = !!v;
return CONFIG.VU_HEADER_SHOW_VALUE;
}, null, null, true);
h('opt_ppmDinColNorm', v => {
CONFIG.PPM_DIN_COLOR_NORMAL = v;
notifyPhoenixGlobalConfig();
return v;
});
h('opt_ppmDinColWarn', v => {
CONFIG.PPM_DIN_COLOR_WARN = v;
notifyPhoenixGlobalConfig();
return v;
});
h('opt_ppmDinHeaderValue', v => {
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');
const offEl = E('opt_ppmDinLoudOff');
const acked = (() => {
try { return localStorage.getItem(PPM_LOUD_WARN_KEY) === '1'; }
catch (_) { return false; }
})();
if (desired && !acked) {
if (cb) cb.checked = false;
CONFIG.PPM_DIN_LOUDNESS_BOXES = false;
if (offEl) offEl.disabled = true;
showPpmLoudnessWarning(() => {
CONFIG.PPM_DIN_LOUDNESS_BOXES = true;
if (cb) cb.checked = true;
if (offEl) offEl.disabled = false;
notifyPhoenixGlobalConfig();
});
return false;
}
CONFIG.PPM_DIN_LOUDNESS_BOXES = desired;
if (offEl) offEl.disabled = !desired;
notifyPhoenixGlobalConfig();
return desired;
}, null, null, true);
h('opt_ppmDinLoudOff', v => {
CONFIG.PPM_DIN_LOUDNESS_OFFSET_DB = clamp(+v, -7, +7);
notifyPhoenixGlobalConfig();
return CONFIG.PPM_DIN_LOUDNESS_OFFSET_DB;
}, 'val_ppmDinLoudOff', (v)=>`${Number(v).toFixed(1)} dB`);
h('opt_ppmEbuColNorm', v => {
CONFIG.PPM_EBU_COLOR_NORMAL = v;
notifyPhoenixGlobalConfig();
return v;
});
h('opt_ppmEbuColWarn', v => {
CONFIG.PPM_EBU_COLOR_WARN = v;
notifyPhoenixGlobalConfig();
return v;
});
h('opt_ppmEbuHeaderValue', v => {
CONFIG.PPM_EBU_HEADER_SHOW_VALUE = !!v;
return CONFIG.PPM_EBU_HEADER_SHOW_VALUE;
}, null, null, true);
h('opt_hifiAlign', v => {
const mode = (String(v) === 'ppm_din_zero') ? 'ppm_din_zero' : 'ppm_din_minus5';
CONFIG.HIFI_PEAK_ALIGNMENT = mode;
invalidateMeters(env);
return mode;
});
h('opt_tpOff', v => CONFIG.TP_OFFSET_DB = clamp(+v, -20, +20));
h('opt_tpHeaderValue', v => {
CONFIG.TP_HEADER_SHOW_VALUE = !!v;
return CONFIG.TP_HEADER_SHOW_VALUE;
}, null, null, true);
h('opt_rmsOff', v => CONFIG.RMS_OFFSET_DB = clamp(+v, -20, +20));
h('opt_rmsHeaderValue', v => {
CONFIG.RMS_HEADER_SHOW_VALUE = !!v;
return CONFIG.RMS_HEADER_SHOW_VALUE;
}, null, null, true);
h('opt_rmsTc', v => {
const mode = String(v).toLowerCase();
if (mode === 'impulse' || mode === 'fast' || mode === 'slow' || mode === 'none') {
CONFIG.RMS_TC_MODE = mode;
CONFIG.RMS_TC_MS = null;
}
return CONFIG.RMS_TC_MODE;
});
// NEW: RMS-Modus + Referenzwerte
h('opt_rmsMode', v => {
const mode = String(v).toLowerCase() === 'dbfs' ? 'dbfs' : 'dbu';
CONFIG.RMS_MODE = mode;
});
h('opt_rmsRefDbfs', v => { CONFIG.RMS_REF_DBFS_FOR_REF_DBU = clamp(+v, -60, 0); });
h('opt_rmsRefDbu', v => { CONFIG.RMS_REF_DBU = clamp(+v, -20, +24); });
h('opt_vuRedThr', v => CONFIG.VU_RED_START = clamp(+v, -20, +12));
h('opt_tpRedThr', v => CONFIG.TP_RED_START = clamp(+v, -20, +6));
// RMS: Range jetzt breit genug für analog (+24 dBu) und digital (0 dBFS)
h('opt_rmsRedThr', v => CONFIG.RMS_RED_START = clamp(+v, -60, +24));
h('opt_vuRedBarOnly', v => CONFIG.VU_RED_BAR_ONLY = !!v, null, null, true);
h('opt_tpRedBarOnly', v => CONFIG.TP_RED_BAR_ONLY = !!v, null, null, true);
h('opt_rmsRedBarOnly', v => CONFIG.RMS_RED_BAR_ONLY = !!v, null, null, true);
h('opt_vuColNorm', v => {
CONFIG.VU_COLOR_NORMAL = v;
notifyPhoenixGlobalConfig();
});
h('opt_vuColWarn', v => {
CONFIG.VU_COLOR_WARN = v;
notifyPhoenixGlobalConfig();
});
h('opt_tpColNorm', v => {
CONFIG.TP_COLOR_NORMAL = v;
notifyPhoenixGlobalConfig();
});
h('opt_tpColWarn', v => {
CONFIG.TP_COLOR_WARN = v;
notifyPhoenixGlobalConfig();
});
h('opt_rmsColNorm', v => {
CONFIG.RMS_COLOR_NORMAL = v;
notifyPhoenixGlobalConfig();
});
h('opt_rmsColWarn', v => {
CONFIG.RMS_COLOR_WARN = v;
notifyPhoenixGlobalConfig();
});
h('opt_corrResponse', v => {
CONFIG.CORR_RESPONSE_S = Number(v) >= 1.75 ? 2.5 : 1.0;
try { env?.notifyRtaConfig?.(); } catch (_) {}
return String(CONFIG.CORR_RESPONSE_S);
});
h('opt_corrNegativeMarkerMode', v => {
const mode = String(v || '').toLowerCase();
CONFIG.CORR_NEGATIVE_MARKER_MODE = ['memory', 'hold', 'off'].includes(mode)
? mode
: 'memory';
return CONFIG.CORR_NEGATIVE_MARKER_MODE;
});
const corrResetPeak = E('opt_corrResetPeak');
if (corrResetPeak) corrResetPeak.onclick = () => {
CONFIG.CORR_RESET_TOKEN = Math.max(0, Math.floor(Number(CONFIG.CORR_RESET_TOKEN) || 0)) + 1;
saveConfig();
try { env?.notifyRtaConfig?.(); } catch (_) {}
};
h('opt_xyPoints', v => {
const raw = Number(v);
CONFIG.XY_POINTS = [128, 256, 512, 1024, 2048].includes(raw)
? raw
: (raw < 192 ? 128 : (raw < 384 ? 256 : (raw < 768 ? 512 : (raw < 1536 ? 1024 : 2048))));
notifyPhoenixGlobalConfig();
return CONFIG.XY_POINTS;
});
h('opt_xyStyle', v => CONFIG.XY_STYLE = (v === 'lines' ? 'lines' : 'points'));
h('opt_xySilenceGate', v => {
CONFIG.XY_SILENCE_GATE_ENABLED = !!v;
return CONFIG.XY_SILENCE_GATE_ENABLED;
}, null, null, true);
h('opt_xySilenceThr', v => {
let val = Number(v);
if (!Number.isFinite(val)) val = -75;
val = Math.max(-90, Math.min(-40, val));
CONFIG.XY_SILENCE_THRESHOLD_RMS_DBFS = val;
CONFIG.CORR_SILENCE_THRESHOLD_RMS_DBFS = val;
return val;
});
h('opt_barThin', v => {
CONFIG.METER_BAR_THIN = clamp(+v, 0.35, 0.9);
return CONFIG.METER_BAR_THIN;
}, 'val_barThin', v=>Number(v).toFixed(2));
h('opt_headerTextColor', v => {
CONFIG.HEADER_TEXT_COLOR = v || '#ffe066';
notifyPhoenixGlobalConfig();
return CONFIG.HEADER_TEXT_COLOR;
});
h('opt_goniGain', (v, el) => {
let val = clamp(+v, -35, 35);
if (!Number.isFinite(val)) val = 0;
val = Math.round(val / 5) * 5;
CONFIG.GONIO_MANUAL_GAIN_DB = val;
if (!CONFIG.GONIO_AGC_ENABLED) {
CONFIG.GONIO_DISPLAY_GAIN_DB = val;
notifyPhoenixGlobalConfig();
}
if (el && el instanceof HTMLElement) {
el.dataset.lastManualGain = String(val);
}
return val;
}, 'val_goniGain', v => `${Number(v).toFixed(0)} dB`);
h('opt_goniAgc', v => {
const targetState = !!v;
const gainSliderEl = E('opt_goniGain');
const gainLabel = E('val_goniGain');
const hudToggle = document.getElementById('goniAgcChk');
const sanitizeGain = (val) => {
let num = Number(val);
if (!Number.isFinite(num)) num = 0;
num = Math.max(-35, Math.min(35, Math.round(num / 5) * 5));
return num;
};
const wasEnabled = !!CONFIG.GONIO_AGC_ENABLED;
const sliderStored = gainSliderEl && gainSliderEl.dataset && typeof gainSliderEl.dataset.lastManualGain === 'string'
? sanitizeGain(gainSliderEl.dataset.lastManualGain)
: null;
const sliderVal = (!wasEnabled && gainSliderEl) ? sanitizeGain(gainSliderEl.value) : null;
if (targetState) {
const manual = sliderVal ?? sliderStored ?? sanitizeGain(CONFIG.GONIO_DISPLAY_GAIN_DB ?? CONFIG.GONIO_MANUAL_GAIN_DB);
CONFIG.GONIO_MANUAL_GAIN_DB = manual;
CONFIG.GONIO_DISPLAY_GAIN_DB = 0;
CONFIG.GONIO_AGC_ENABLED = true;
if (gainSliderEl) {
gainSliderEl.disabled = true;
gainSliderEl.value = '0';
gainSliderEl.dataset.lastManualGain = String(manual);
}
if (gainLabel) gainLabel.textContent = '0 dB';
} else {
const manual = sanitizeGain(CONFIG.GONIO_MANUAL_GAIN_DB ?? sliderStored ?? sliderVal ?? CONFIG.GONIO_DISPLAY_GAIN_DB);
CONFIG.GONIO_MANUAL_GAIN_DB = manual;
CONFIG.GONIO_DISPLAY_GAIN_DB = manual;
CONFIG.GONIO_AGC_ENABLED = false;
notifyPhoenixGlobalConfig();
if (gainSliderEl) {
gainSliderEl.disabled = false;
gainSliderEl.value = String(manual);
gainSliderEl.dataset.lastManualGain = String(manual);
}
if (gainLabel) gainLabel.textContent = `${manual} dB`;
}
if (gainSliderEl) gainSliderEl.disabled = !!CONFIG.GONIO_AGC_ENABLED;
if (hudToggle) hudToggle.checked = !!CONFIG.GONIO_AGC_ENABLED;
}, null, null, true);
h('opt_phaseGain', v => {
let val = clamp(+v, -35, 35);
if (!Number.isFinite(val)) val = 0;
val = Math.round(val / 5) * 5;
CONFIG.PHASE_DISPLAY_GAIN_DB = val;
notifyPhoenixGlobalConfig();
return val;
}, 'val_phaseGain', v => `${Number(v).toFixed(0)} dB`);
h('opt_phaseAgc', v => {
if (CONFIG.PHASE_AMPLITUDE_MODE === 'ppm-din') {
CONFIG.PHASE_AGC_ENABLED = false;
enforcePhaseAmplitudeConstraints({ persist: false });
return false;
}
CONFIG.PHASE_AGC_ENABLED = !!v;
const gainSliderEl = E('opt_phaseGain');
if (CONFIG.PHASE_AGC_ENABLED) {
CONFIG.PHASE_DISPLAY_GAIN_DB = 0;
if (gainSliderEl) gainSliderEl.value = 0;
const gainLabel = E('val_phaseGain');
if (gainLabel) gainLabel.textContent = '0 dB';
}
if (gainSliderEl) gainSliderEl.disabled = CONFIG.PHASE_AGC_ENABLED;
notifyPhoenixGlobalConfig();
}, null, null, true);
h('opt_phaseAmpMode', v => {
const mode = (v === 'ppm-din') ? 'ppm-din' : 'bandpass';
CONFIG.PHASE_AMPLITUDE_MODE = mode;
enforcePhaseAmplitudeConstraints({ persist: false });
notifyPhoenixGlobalConfig();
return mode;
});
h('opt_phaseTrail', v => {
CONFIG.PHASE_TRAIL_ENABLED = !!v;
const hudToggle = document.getElementById('phaseTrailChk');
if (hudToggle) hudToggle.checked = CONFIG.PHASE_TRAIL_ENABLED;
notifyPhoenixGlobalConfig();
}, null, null, true);
h('opt_goniGap', v => CONFIG.GONI_METER_GAP = clamp(+v, 2, 24), 'val_goniGap', v=>`${v} px`);
h('opt_goniPersistence', v => {
const mode = String(v).toLowerCase();
CONFIG.GONIO_PERSISTENCE_MODE = ['fast', 'medium', 'slow', 'custom'].includes(mode) ? mode : 'fast';
return CONFIG.GONIO_PERSISTENCE_MODE;
});
h('opt_goniFade', v => {
CONFIG.GONIO_LINE_FADE_MS = clamp(+v, 0, 600);
CONFIG.GONIO_PERSISTENCE_MODE = 'custom';
const persistence = E('opt_goniPersistence');
if (persistence) persistence.value = 'custom';
}, 'val_goniFade', v=>`${Math.round(v)} ms`);
h('opt_panelDividers', v => {
CONFIG.PANEL_DIVIDERS_ENABLED = !!v;
return CONFIG.PANEL_DIVIDERS_ENABLED;
}, null, null, true);
// Hold-/Decay-Parameter
h('opt_rtaEngine', v => {
CONFIG.RTA_ENGINE = CONFIG.RTA_BAR_LAYOUT === 'rtw' ? 'iir' : ((v === 'iir') ? 'iir' : 'fft');
return CONFIG.RTA_ENGINE;
});
h('opt_rtaFilterbank', v => {
CONFIG.RTA_IIR_FILTERBANK = v === 'butterworth' ? 'butterworth' : 'legacy';
return CONFIG.RTA_IIR_FILTERBANK;
});
h('opt_rtaBpo', v => {
applyRtaBpoSelection(v);
const layoutControl = E('opt_rtaLayout');
if (layoutControl) layoutControl.value = CONFIG.RTA_BAR_LAYOUT;
notifyPhoenixGlobalConfig();
return CONFIG.RTA_BPO_MODE;
});
h('opt_rtaOrder', v => {
if (CONFIG.RTA_BAR_LAYOUT === 'rtw') {
CONFIG.RTA_IIR_ORDER = 6;
return 6;
}
let val = Math.round(+v);
if (!Number.isFinite(val)) val = 4;
val = Math.max(2, Math.min(8, val));
if (val % 2 !== 0) val += 1;
CONFIG.RTA_IIR_ORDER = val;
return val;
});
h('opt_rtaFreq', v => {
CONFIG.RTA_FREQ_RANGE = CONFIG.RTA_BAR_LAYOUT === 'rtw' ? 'norm' : ((v === 'lf') ? 'lf' : 'norm');
return CONFIG.RTA_FREQ_RANGE;
});
h('opt_rtaDisplayGainFft', v => { CONFIG.RTA_DISPLAY_GAIN_FFT_DB = clamp(+v, -25, 25); }, 'val_rtaDisplayGainFft', v=>`${v} dB`);
h('opt_rtaDisplayGainIir', v => { CONFIG.RTA_DISPLAY_GAIN_IIR_DB = clamp(+v, -25, 25); }, 'val_rtaDisplayGainIir', v=>`${v} dB`);
h('opt_rtaLayout', v => {
const val = (v === 'rtw') ? 'rtw' : 'iec';
CONFIG.RTA_BAR_LAYOUT = val;
if (val === 'rtw') {
CONFIG.RTA_ENGINE = 'iir';
CONFIG.RTA_IIR_ORDER = 6;
CONFIG.RTA_FREQ_RANGE = 'norm';
const forced = {
opt_rtaEngine: 'iir',
opt_rtaOrder: '6',
opt_rtaFreq: 'norm',
};
for (const [id, value] of Object.entries(forced)) {
const control = document.getElementById(id);
if (control) control.value = value;
}
notifyPhoenixGlobalConfig();
}
return val;
});
h('opt_rtaBarBaseColor', v => {
CONFIG.RTA_BAR_BASE_COLOR = v || '#ffe066';
notifyPhoenixGlobalConfig();
return CONFIG.RTA_BAR_BASE_COLOR;
});
h('opt_rtaWeighting', v => {
const allowed = new Set(['z','a','c']);
CONFIG.RTA_WEIGHTING = allowed.has(v) ? v : 'z';
});
h('opt_rtaDetector', v => {
CONFIG.RTA_DETECTOR = v === 'peak' ? 'peak' : 'average';
const response = E('opt_rtaIntegration');
if (response) response.disabled = CONFIG.RTA_DETECTOR === 'peak';
return CONFIG.RTA_DETECTOR;
});
h('opt_rtaIntegration', v => {
const allowed = new Set(['impulse','fast','medium','slow']);
CONFIG.RTA_INTEGRATION = allowed.has(v) ? v : 'fast';
return CONFIG.RTA_INTEGRATION;
});
h('opt_rtaBallistics', v => {
const val = (v === 'peak' || v === 'both' || v === 'average') ? v : 'average';
CONFIG.RTA_BALLISTICS_MODE = val;
return val;
});
h('opt_rtaHoldMode', v => {
const allowed = new Set(['off','auto','manual']);
CONFIG.RTA_PEAK_HOLD_MODE = allowed.has(v) ? v : 'auto';
});
h('opt_rtaHoldTime', v => {
const requested = clamp(+v, 0, 30);
CONFIG.RTA_PEAK_HOLD_SEC = CONFIG.RTA_BAR_LAYOUT === 'rtw'
? (requested < 3.25 ? 2.5 : 4.0)
: requested;
return CONFIG.RTA_PEAK_HOLD_SEC;
});
h('opt_rtaDisplayHold', v => { CONFIG.RTA_DISPLAY_HOLD_SEC = clamp(+v, 0, 5); });
h('opt_spectroGamma', v => {
const num = clamp(parseFloat(v), 0.3, 1.2);
CONFIG.SPECTRO_GAMMA = num;
notifyPhoenixGlobalConfig();
return num;
}, 'val_spectroGamma', (v)=>Number(v).toFixed(2));
h('opt_spectroScroll', v => {
const allowed = new Set([0.5,1,2,4,6]);
const num = Number(v);
const val = allowed.has(num) ? num : 1;
CONFIG.SPECTRO_SCROLL_MODE = val;
notifyPhoenixGlobalConfig();
return val;
});
h('opt_peakHistScroll', v => {
const allowed = new Set([0.5,1,2,4,6]);
const num = Number(v);
const val = allowed.has(num) ? num : 1;
CONFIG.PEAK_HISTORY_SCROLL_MODE = val;
notifyPhoenixGlobalConfig();
return val;
});
h('opt_peakHistFill', v => {
CONFIG.PEAK_HISTORY_FILL_ENABLED = !!v;
notifyPhoenixGlobalConfig();
return CONFIG.PEAK_HISTORY_FILL_ENABLED;
}, null, null, true);
h('opt_peakHistFillInvert', v => {
CONFIG.PEAK_HISTORY_FILL_INVERT = !!v;
notifyPhoenixGlobalConfig();
return CONFIG.PEAK_HISTORY_FILL_INVERT;
}, null, null, true);
h('opt_waveMode', v => {
const mode = (v === 'overlay') ? 'overlay' : (v === 'diff' ? 'diff' : 'stacked');
CONFIG.WAVEFORM_MODE = mode;
return mode;
});
h('opt_waveWindow', v => {
const options = [0.05, 0.1, 0.5, 1, 2, 5, 10, 15];
const num = clamp(parseFloat(v), 0.05, 15);
const closest = options.reduce((prev, curr) => Math.abs(curr - num) < Math.abs(prev - num) ? curr : prev, options[0]);
CONFIG.WAVEFORM_WINDOW_SEC = closest;
return closest;
});
h('opt_waveColorL', v => {
CONFIG.WAVEFORM_COLOR_LEFT = v;
notifyPhoenixGlobalConfig();
});
h('opt_waveColorR', v => {
CONFIG.WAVEFORM_COLOR_RIGHT = v;
notifyPhoenixGlobalConfig();
});
h('opt_waveColorDiff', v => {
CONFIG.WAVEFORM_COLOR_DIFF = v;
notifyPhoenixGlobalConfig();
});
h('opt_rtaTauFast', () => { CONFIG.RTA_IIR_TAU_FAST = 0.125; return 0.125; });
h('opt_rtaTauSlow', () => { CONFIG.RTA_IIR_TAU_SLOW = 1.0; return 1.0; });
h('opt_rtRenderMode', v => { CONFIG.REALTIME_RENDER_STYLE = (v === 'line') ? 'line' : 'bars'; });
h('opt_rtHold', v => CONFIG.REALTIME_BAR_HOLD_MS = clamp(+v, 100, 5000));
h('opt_rtDecay', v => CONFIG.REALTIME_BAR_DECAY_DB_PER_S = clamp(+v, 1, 60));
h('opt_vuHold', v => CONFIG.VU_HOLD_MS = clamp(+v, 100, 5000));
h('opt_vuDecay', v => CONFIG.VU_DECAY_DB_PER_S = clamp(+v, 5, 60));
h('opt_tpHold', v => CONFIG.TP_HOLD_MS = clamp(+v, 0, 5000));
h('opt_tpDecay', v => CONFIG.TP_DECAY_DB_PER_S = clamp(+v, 1, 60));
// LUFS
h('opt_lufsRedThr', v => CONFIG.LUFS_RED_START = clamp(+v, -10, 0));
h('opt_lufsYellowThr', v => CONFIG.LUFS_YELLOW_START = clamp(+v, -10, 0));
h('opt_lufsGreenThr', v => CONFIG.LUFS_GREEN_START = clamp(+v, -10, 0));
h('opt_lufsColI', v => {
CONFIG.LUFS_COLOR_I = v;
notifyPhoenixGlobalConfig();
});
h('opt_lufsColM', v => {
CONFIG.LUFS_COLOR_M = v;
notifyPhoenixGlobalConfig();
});
h('opt_lufsColS', v => {
CONFIG.LUFS_COLOR_S = v;
notifyPhoenixGlobalConfig();
});
h('opt_lufsScaleCol', v => {
CONFIG.LUFS_SCALE_LABEL_COLOR = v;
notifyPhoenixGlobalConfig();
});
h('opt_lufsIWindowMin', v => {
const snapped = Math.round(Number(v));
CONFIG.LUFS_I_WINDOW_MIN = clamp(snapped, 1, 10);
notifyPhoenixGlobalConfig();
return CONFIG.LUFS_I_WINDOW_MIN;
}, 'val_lufsIWindowMin', (v)=>`${Number(v).toFixed(0)} min`);
h('opt_lufsINorm', v => {
CONFIG.LUFS_I_NORM_ENABLED = !!v;
const win = E('opt_lufsIWindowMin');
if (win) win.disabled = !!CONFIG.LUFS_I_NORM_ENABLED;
notifyPhoenixGlobalConfig();
return CONFIG.LUFS_I_NORM_ENABLED;
}, null, null, true);
h('opt_stopwatchDisplay', v => {
CONFIG.STOPWATCH_DISPLAY_STYLE = (v === 'seven') ? 'seven' : 'mono';
return CONFIG.STOPWATCH_DISPLAY_STYLE;
});
wireLinkedThresholdPair(
'opt_stopwatchThreshDbfs',
'opt_stopwatchThreshPpmDin',
() => CONFIG.STOPWATCH_AUTO_THRESHOLD_DBFS,
(v) => { CONFIG.STOPWATCH_AUTO_THRESHOLD_DBFS = v; },
-70
);
h('opt_recFormat', v => {
CONFIG.RECORD_OUTPUT_FORMAT = (v === 'webm') ? 'webm' : ((v === 'mp3') ? 'mp3' : 'wav');
notifyPhoenixGlobalConfig();
syncUI();
return CONFIG.RECORD_OUTPUT_FORMAT;
});
h('opt_recMp3Bitrate', v => {
const n = Number(v);
CONFIG.RECORD_MP3_BITRATE_KBPS = [64, 128, 192, 256, 320].includes(n) ? n : 192;
notifyPhoenixGlobalConfig();
return String(CONFIG.RECORD_MP3_BITRATE_KBPS);
});
h('recTargetSel', v => {
CONFIG.RECORD_TARGET = (v === 'analyzer' || v === 'volumio') ? v : 'download';
if (CONFIG.RECORD_TARGET === 'analyzer' || CONFIG.RECORD_TARGET === 'volumio') {
CONFIG.RECORD_PI_TARGET = CONFIG.RECORD_TARGET;
notifyPhoenixGlobalConfig();
}
return CONFIG.RECORD_TARGET;
});
h('opt_recAutoDownload', v => {
CONFIG.RECORD_AUTO_DOWNLOAD = !!v;
return CONFIG.RECORD_AUTO_DOWNLOAD;
}, null, null, true);
h('opt_recShowAb', v => {
CONFIG.RECORD_SHOW_RECORDER_AB = !!v;
return CONFIG.RECORD_SHOW_RECORDER_AB;
}, null, null, true);
h('opt_recAutoGap', v => {
const n = Number(v);
const clamped = Math.max(0.5, Math.min(3, Number.isFinite(n) ? n : 1.5));
const stepped = Math.round(clamped * 2) / 2;
CONFIG.RECORD_AUTO_SPLIT_GAP_SEC = stepped;
notifyPhoenixGlobalConfig();
return stepped;
}, 'val_recAutoGap', (v)=>`${Number(v).toFixed(1)} s`);
wireLinkedThresholdPair(
'opt_recThreshDbfs',
'opt_recThreshPpmDin',
() => CONFIG.RECORD_AUTO_THRESHOLD_DBFS,
(v) => { CONFIG.RECORD_AUTO_THRESHOLD_DBFS = v; },
-50,
() => { notifyPhoenixGlobalConfig(); }
);
const normSplitPlot = (v, fallback) => {
const id = String(v || '');
const ok =
id === 'none' ||
id === 'phase-wheel' ||
id === 'realtime' ||
id === 'goniometer-rtw' ||
id === 'peak-history' ||
id === 'classic-needles' ||
id === 'panel' ||
id === 'clock' ||
id === 'waveform' ||
id === 'spectrogram';
return ok ? id : fallback;
};
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;
};
h('opt_splitTapAction', v => { CONFIG.SPLIT_VIEW_TAP_ACTION = (String(v) === 'popup') ? 'popup' : 'switch'; });
h('opt_splitLeftPlot', v => { CONFIG.SPLIT_VIEW_LEFT = normSplitPlot(v, 'phase-wheel'); });
h('opt_splitRightPlot', v => { CONFIG.SPLIT_VIEW_RIGHT = normSplitPlot(v, 'realtime'); });
h('opt_quadTapAction', v => { CONFIG.QUAD_VIEW_TAP_ACTION = (String(v) === 'popup') ? 'popup' : 'switch'; });
h('opt_quadPlotTL', v => { CONFIG.QUAD_VIEW_TL = normSplitPlot(v, 'phase-wheel'); });
h('opt_quadPlotTR', v => { CONFIG.QUAD_VIEW_TR = normSplitPlot(v, 'realtime'); });
h('opt_quadPlotBL', v => { CONFIG.QUAD_VIEW_BL = normSplitPlot(v, 'goniometer-rtw'); });
h('opt_quadPlotBR', v => { CONFIG.QUAD_VIEW_BR = normSplitPlot(v, 'none'); });
h('opt_quadMeterCount', v => {
const raw = Number(v);
const n = Number.isFinite(raw) ? raw : 0;
CONFIG.QUAD_VIEW_METER_COUNT = clamp(n | 0, 0, 3);
applyQuadViewOptionsDisabled();
try { env?.syncSplitView?.(); } catch (_) {}
return CONFIG.QUAD_VIEW_METER_COUNT;
});
h('opt_quadMeter1', v => { CONFIG.QUAD_VIEW_METER_1 = normSplitMeter(v, 'vu'); });
h('opt_quadMeter1Pos', v => { CONFIG.QUAD_VIEW_METER_1_POS = normSplitPos(v, 'right'); });
h('opt_quadMeter2', v => { CONFIG.QUAD_VIEW_METER_2 = normSplitMeter(v, 'ppm-din'); });
h('opt_quadMeter2Pos', v => { CONFIG.QUAD_VIEW_METER_2_POS = normSplitPos(v, 'right'); });
h('opt_quadMeter3', v => { CONFIG.QUAD_VIEW_METER_3 = normSplitMeter(v, 'lufs'); });
h('opt_quadMeter3Pos', v => { CONFIG.QUAD_VIEW_METER_3_POS = normSplitPos(v, 'right'); });
h('opt_splitMeterCount', v => {
const raw = Number(v);
const n = Number.isFinite(raw) ? raw : 0;
CONFIG.SPLIT_VIEW_METER_COUNT = clamp(n | 0, 0, 3);
applySplitViewOptionsDisabled();
return CONFIG.SPLIT_VIEW_METER_COUNT;
});
h('opt_splitMeter1', v => { CONFIG.SPLIT_VIEW_METER_1 = normSplitMeter(v, 'vu'); });
h('opt_splitMeter1Pos', v => { CONFIG.SPLIT_VIEW_METER_1_POS = normSplitPos(v, 'right'); });
h('opt_splitMeter2', v => { CONFIG.SPLIT_VIEW_METER_2 = normSplitMeter(v, 'ppm-din'); });
h('opt_splitMeter2Pos', v => { CONFIG.SPLIT_VIEW_METER_2_POS = normSplitPos(v, 'right'); });
h('opt_splitMeter3', v => { CONFIG.SPLIT_VIEW_METER_3 = normSplitMeter(v, 'lufs'); });
h('opt_splitMeter3Pos', v => { CONFIG.SPLIT_VIEW_METER_3_POS = normSplitPos(v, 'right'); });
const applyPresetRefresh = (uiState = null) => {
try { env?.applyPresetUiState?.(uiState); } catch (_) {}
syncUI();
try { env?.updateInputOffset?.(); } catch (_) {}
try { env?.updateMonoInput?.(); } catch (_) {}
try { env?.updateLrDelay?.(); } catch (_) {}
try { env?.notifyPhoenixGlobalConfig?.(); } catch (_) {}
try { env?.notifyRtaConfig?.(); } catch (_) {}
try { env?.notifyPpmConfig?.(); } catch (_) {}
try { env?.syncRealtimeGain?.(); } catch (_) {}
try { env?.syncWaveformWindow?.(); } catch (_) {}
try { env?.syncWaveformMode?.(); } catch (_) {}
try { env?.syncRtaBpo?.(); } catch (_) {}
try { env?.syncSplitView?.(); } catch (_) {}
const rawStyle = String(uiState?.style || CONFIG.UI_LAST_STYLE || '');
const fallbackStyle = String(uiState?.lastNonOptionStyle || CONFIG.UI_LAST_NON_OPTION_STYLE || 'realtime');
const targetStyle = (rawStyle === 'options-panel' || rawStyle === 'options') ? fallbackStyle : (rawStyle || fallbackStyle);
try { env?.switchView?.(targetStyle); } catch (_) {}
invalidateMeters(env);
try { env?.requestRender?.(); } catch (_) {}
};
E('opt_softwarePreset')?.addEventListener('change', (e) => {
const next = String(e.target?.value || 'default').toLowerCase();
CONFIG.SOFTWARE_PRESET = next;
saveConfig();
syncUI();
});
E('btnSoftwarePresetLoad')?.addEventListener('click', async () => {
const presetId = String(E('opt_softwarePreset')?.value || CONFIG.SOFTWARE_PRESET || 'default');
try {
const result = await loadSoftwarePreset(presetId);
applyPresetRefresh(result?.uiState || result?.snapshot?.__PHOENIX_UI_STATE || null);
setTimeout(() => {
try { globalThis?.location?.reload?.(); } catch (_) {}
}, 30);
} catch (err) {
console.error('Preset load error:', err);
}
});
E('btnSoftwarePresetSave')?.addEventListener('click', async () => {
const presetId = String(E('opt_softwarePreset')?.value || CONFIG.SOFTWARE_PRESET || 'default');
if (presetId === 'default') return;
if (!isPresetSaveLocalhostContext()) {
showPresetSaveRemoteNotice();
return;
}
try {
const snapshot = env?.buildPresetSnapshot?.() || null;
await saveSoftwarePreset(presetId, snapshot);
syncUI();
} catch (err) {
console.error('Preset save error:', err);
}
});
E('btnLayoutPresetSave')?.addEventListener('click', async () => {
const layoutId = String(E('opt_layoutPreset')?.value || '1');
if (!isPresetSaveLocalhostContext()) {
showPresetSaveRemoteNotice();
return;
}
try {
const uiState = env?.buildPresetUiState?.() || null;
await saveLayoutPreset(layoutId, uiState);
} catch (err) {
console.error('Layout preset save error:', err);
}
});
const activateLayoutPreset = async (layoutId) => {
try {
const result = await loadLayoutPreset(layoutId);
if (!result) return;
const sel = E('opt_layoutPreset');
if (sel) sel.value = String(layoutId);
applyPresetRefresh(result?.uiState || result?.snapshot?.__PHOENIX_UI_STATE || null);
setTimeout(() => {
try { globalThis?.location?.reload?.(); } catch (_) {}
}, 30);
} catch (err) {
console.error('Layout preset load error:', err);
}
};
E('btnLayoutPresetLoad1')?.addEventListener('click', async () => { await activateLayoutPreset('1'); });
E('btnLayoutPresetLoad2')?.addEventListener('click', async () => { await activateLayoutPreset('2'); });
E('btnLayoutPresetLoad3')?.addEventListener('click', async () => { await activateLayoutPreset('3'); });
E('btnLayoutPresetLoad4')?.addEventListener('click', async () => { await activateLayoutPreset('4'); });
E('btnSoftwarePresetExport')?.addEventListener('click', async () => {
const presetId = String(E('opt_softwarePreset')?.value || CONFIG.SOFTWARE_PRESET || 'default');
try {
const payload = await exportSoftwarePreset(presetId);
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
const a = Object.assign(document.createElement('a'), {
href: URL.createObjectURL(blob),
download: `analyzer-preset-${payload.presetId}.json`,
});
a.click();
URL.revokeObjectURL(a.href);
} catch (err) {
console.error('Preset export error:', err);
}
});
E('impSoftwarePreset')?.addEventListener('change', async (e) => {
const file = e.target.files?.[0];
if (!file) return;
const presetId = String(E('opt_softwarePreset')?.value || CONFIG.SOFTWARE_PRESET || 'default');
if (presetId === 'default') {
e.target.value = '';
return;
}
try {
const payload = JSON.parse(await file.text());
const result = await importSoftwarePreset(presetId, payload);
applyPresetRefresh(result?.uiState || result?.snapshot?.__PHOENIX_UI_STATE || null);
setTimeout(() => {
try { globalThis?.location?.reload?.(); } catch (_) {}
}, 30);
} catch (err) {
console.error('Preset import error:', err);
} finally {
e.target.value = '';
}
});
E('btnReloadAudio')?.addEventListener('click', () => env.audioReload?.());
E('btnOnlineUpdate')?.addEventListener('click', async () => {
const btn = E('btnOnlineUpdate');
const original = btn?.textContent || 'Online Update';
if (btn) {
btn.disabled = true;
btn.textContent = 'Lade...';
}
try {
const response = await fetch(`${resolvePhoenixApiBaseUrl()}/api/v1/update/download`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
});
const payload = await response.json().catch(() => null);
if (!response.ok || payload?.ok === false) {
throw new Error(payload?.error || `HTTP ${response.status}`);
}
showOnlineUpdateReadyNotice();
} catch (err) {
console.error('Online update error:', err);
try { globalThis?.alert?.(`Online Update fehlgeschlagen: ${err?.message || String(err)}`); } catch (_) {}
} finally {
if (btn) {
btn.disabled = false;
btn.textContent = original;
}
}
});
E('btnReset')?.addEventListener('click', () => {
showFactoryResetConfirm();
});
}
// ---- Reset -----------------------------------------------------
function resetDefaults() {
resetConfigToDefaults();
}
function isPresetSaveLocalhostContext() {
const host = String(globalThis?.location?.hostname || '').trim().toLowerCase();
return host === 'localhost' || host === '127.0.0.1' || host === '::1';
}
function resolvePhoenixApiBaseUrl() {
const host = String(globalThis?.location?.hostname || '').trim() || '127.0.0.1';
const fallback = `http://${host}:8789`;
const input = String(CONFIG?.PHOENIX_BASE_URL || '').trim() || fallback;
try {
const url = new URL(input, fallback);
const currentHost = host.toLowerCase();
const targetHost = String(url.hostname || '').trim().toLowerCase();
const isLoopback = (value) => value === 'localhost' || value === '127.0.0.1' || value === '::1' || value === '[::1]';
if (!isLoopback(currentHost) && isLoopback(targetHost)) {
url.hostname = host;
}
return url.origin;
} catch (_) {
return fallback;
}
}
function showPresetSaveRemoteNotice() {
const wrap = E('presetSaveRemoteNotice');
const btnOk = E('presetSaveRemoteOkBtn');
if (!wrap || !btnOk) return;
const close = () => {
wrap.style.display = 'none';
btnOk.removeEventListener('click', onOk);
wrap.removeEventListener('click', onBackdrop);
};
const onOk = (e) => {
if (e && typeof e.preventDefault === 'function') e.preventDefault();
if (e && typeof e.stopPropagation === 'function') e.stopPropagation();
close();
};
const onBackdrop = (e) => {
if (e.target === wrap) close();
};
btnOk.addEventListener('click', onOk);
wrap.addEventListener('click', onBackdrop);
wrap.style.display = 'flex';
}
function showOnlineUpdateReadyNotice() {
const wrap = E('onlineUpdateReadyNotice');
const btnRestart = E('onlineUpdateRestartBtn');
if (!wrap || !btnRestart) return;
const close = () => {
wrap.style.display = 'none';
btnRestart.removeEventListener('click', onRestart);
wrap.removeEventListener('click', onBackdrop);
};
const onRestart = async (e) => {
if (e && typeof e.preventDefault === 'function') e.preventDefault();
if (e && typeof e.stopPropagation === 'function') e.stopPropagation();
btnRestart.disabled = true;
try {
await fetch(`${resolvePhoenixApiBaseUrl()}/api/v1/update/restart`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
});
} catch (err) {
console.error('Online restart error:', err);
}
close();
};
const onBackdrop = (e) => {
if (e.target === wrap) close();
};
btnRestart.disabled = false;
btnRestart.addEventListener('click', onRestart);
wrap.addEventListener('click', onBackdrop);
wrap.style.display = 'flex';
}
function showFactoryResetConfirm() {
const wrap = E('factoryResetConfirm');
const ack = E('factoryResetAck');
const btnYes = E('factoryResetConfirmBtn');
const btnCancel = E('factoryResetCancelBtn');
if (!wrap || !ack || !btnYes || !btnCancel) {
// Fallback: still allow reset if modal is missing
factoryResetStorage();
try { location.reload(); } catch (_) {}
return;
}
const close = () => {
wrap.style.display = 'none';
};
const sync = () => {
btnYes.disabled = !ack.checked;
};
if (!wrap.dataset.bound) {
ack.addEventListener('change', sync);
btnCancel.addEventListener('click', (e) => {
if (e && typeof e.preventDefault === 'function') e.preventDefault();
if (e && typeof e.stopPropagation === 'function') e.stopPropagation();
close();
});
btnYes.addEventListener('click', (e) => {
if (e && typeof e.preventDefault === 'function') e.preventDefault();
if (e && typeof e.stopPropagation === 'function') e.stopPropagation();
if (!ack.checked) return;
factoryResetStorage();
try { location.reload(); } catch (_) {}
});
wrap.addEventListener('click', (e) => {
if (e.target === wrap) close();
});
wrap.dataset.bound = '1';
}
ack.checked = false;
sync();
wrap.style.display = 'flex';
}
function factoryResetStorage() {
const exactKeys = new Set([
'calibration_notice_seen_v1',
'recorder_warning_ack_v1',
'ppm_din_loudness_warn_ack_v1',
'ppm_din_mode', // legacy alignment storage
]);
const prefixes = ['analyzer_', 'MIGRATE_'];
try {
const keys = [];
for (let i = 0; i < localStorage.length; i++) {
const k = localStorage.key(i);
if (k) keys.push(k);
}
for (const k of keys) {
if (exactKeys.has(k) || prefixes.some((p) => k.startsWith(p))) {
try { localStorage.removeItem(k); } catch (_) {}
}
}
} catch (_) {}
try { sessionStorage.removeItem('calibration_notice_seen_v1'); } catch (_) {}
}
function bindAlignmentToggle(env) {
const dinToggle = E('opt_din_al_minus6');
if (!dinToggle) return;
const syncState = () => {
dinToggle.checked = CONFIG.PPM_DIN_MODE !== 'al_minus9';
};
syncState();
dinToggle.addEventListener('change', () => {
const mode = dinToggle.checked ? 'ebu' : 'ard';
applyAlignmentProfile(mode);
saveConfig();
syncUI();
invalidateMeters(env);
});
}
function bindVuRefToggle(env) {
const vuToggle = E('opt_vu_plus4');
if (!vuToggle) return;
const syncState = () => {
vuToggle.checked = CONFIG.VU_REF_IS_PLUS4 !== false;
};
syncState();
vuToggle.addEventListener('change', () => {
CONFIG.VU_REF_IS_PLUS4 = !!vuToggle.checked;
updateVuReference();
saveConfig();
invalidateMeters(env);
});
}
function invalidateMeters(env) {
try {
if (typeof env?.invalidateMeters === 'function') {
env.invalidateMeters();
return;
}
if (typeof window !== 'undefined' &&
window.__AN_REGISTRY__ &&
typeof window.__AN_REGISTRY__.invalidateAll === 'function') {
window.__AN_REGISTRY__.invalidateAll();
}
} catch (e) {
console.warn('Meter invalidate failed:', e);
}
}