diff --git a/main.js b/main.js index 754da57..ff716b0 100644 --- a/main.js +++ b/main.js @@ -882,6 +882,21 @@ function refreshTrayMenu() { enabled: false }, { type: "separator" }, + { + label: "Global Sensitivity", + submenu: [ + { label: "0.5x (Quiet)", value: 0.5 }, + { label: "1.0x (Normal)", value: 1.0 }, + { label: "1.5x (Loud)", value: 1.5 }, + { label: "2.0x (Aggressive)", value: 2.0 } + ].map((opt) => ({ + label: opt.label, + type: "radio", + checked: visualizerSettings.globalSensitivity === opt.value, + click: () => updateSettings({ globalSensitivity: opt.value }) + })) + }, + { type: "separator" }, { label: isPaused ? "Resume Visualizer" : "Pause Visualizer", click: () => togglePaused() diff --git a/renderer.js b/renderer.js index da2aa3a..2d1a392 100644 --- a/renderer.js +++ b/renderer.js @@ -485,6 +485,9 @@ function applySettings(nextSettings) { } }; + if (!nextSettings) return; + visualizerState.globalSensitivity = nextSettings.globalSensitivity ?? 1.0; + if (!["ambientWave", "reactiveBorder", "flowBorder", "sideBars", "flatRipples", "dotParticles", "rippleFlow", "snowBubbleParticles", "edgeCrystals"].includes(visualizerState.selectedTheme)) { visualizerState.selectedTheme = "ambientWave"; } @@ -655,13 +658,29 @@ if (window.audioBridge) { if (payload && typeof payload.value === "number") { latestSource = payload.source || "unknown"; lastPayloadValue = payload.value; + + // 1. Fetch the sensitivity value from state + let rawSensitivity = visualizerState?.globalSensitivity; + + // 2. Validate and clamp runtime boundaries right before calculation path + let globalSensitivity = 1.0; + if (typeof rawSensitivity === "number" && Number.isFinite(rawSensitivity)) { + globalSensitivity = Math.min(Math.max(rawSensitivity, 0.1), 5.0); + } + + // 3. Scale the incoming audio level safely + const scaledValue = payload.value * globalSensitivity; + + const helperDriven = latestSource === "helper"; incomingLevel = latestSource === "helper" - ? clamp01(payload.value * getActiveAudioMultiplier()) - : clamp01(payload.value); + ? clamp01(scaledValue * getActiveAudioMultiplier()) + : clamp01(scaledValue); } }); } + + if (window.visualizerSettings) { window.visualizerSettings.onChange((nextSettings) => { applySettings(nextSettings); diff --git a/settingsStore.js b/settingsStore.js index 41feba8..53ec4fb 100644 --- a/settingsStore.js +++ b/settingsStore.js @@ -3,6 +3,7 @@ const path = require("path"); const DEFAULT_SETTINGS = Object.freeze({ selectedTheme: "ambientWave", + globalSensitivity: 1.0, ambientWave: Object.freeze({ tone: "blue", sensitivity: "medium", @@ -93,6 +94,7 @@ function createDefaultSettings() { return { selectedTheme: DEFAULT_SETTINGS.selectedTheme, ambientWave: { ...DEFAULT_SETTINGS.ambientWave }, + globalSensitivity: DEFAULT_SETTINGS.globalSensitivity , reactiveBorder: { ...DEFAULT_SETTINGS.reactiveBorder }, flowBorder: { ...DEFAULT_SETTINGS.flowBorder }, sideBars: { ...DEFAULT_SETTINGS.sideBars }, @@ -273,8 +275,16 @@ function migrateLegacySettings(input = {}) { function sanitizeSettings(input = {}) { const source = migrateLegacySettings(input); + // 1. Enforce strict type validation and numeric finite boundaries + let safeSensitivity = 1.0; // Production default fallback + if (typeof source.globalSensitivity === 'number' && Number.isFinite(source.globalSensitivity)) { + // 2. Enforce a safe range clamp between 0.1x (minimum) and 5.0x (maximum) + safeSensitivity = Math.min(Math.max(source.globalSensitivity, 0.1), 5.0); + } + return { selectedTheme: pick(source.selectedTheme, VALID_MAIN_THEMES, DEFAULT_SETTINGS.selectedTheme), + globalSensitivity: safeSensitivity, // Use the securely validated value ambientWave: sanitizeAmbientWave(source.ambientWave), reactiveBorder: sanitizeReactiveBorder(source.reactiveBorder), flowBorder: sanitizeFlowBorder(source.flowBorder),