1755 lines
66 KiB
JavaScript
1755 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_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);
|
|
});
|
|
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_rtaDecay', v => { CONFIG.RTA_PEAK_DECAY_DB_PER_S = clamp(+v, 1, 60); });
|
|
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);
|
|
}
|
|
}
|