// 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_rtaDecay', CONFIG.RTA_PEAK_DECAY_DB_PER_S ?? 20], ['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','fall','manual']); CONFIG.RTA_PEAK_HOLD_MODE = allowed.has(v) ? v : 'fall'; }); h('opt_rtaDecay', v => { CONFIG.RTA_PEAK_DECAY_DB_PER_S = clamp(+v, 1, 60); }); 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); } }