diff --git a/server/alert/current-conditions.mp3 b/server/alert/current-conditions.mp3 new file mode 100644 index 0000000..bc8317b Binary files /dev/null and b/server/alert/current-conditions.mp3 differ diff --git a/server/alert/hourly-forecast.mp3 b/server/alert/hourly-forecast.mp3 new file mode 100644 index 0000000..2e37f61 Binary files /dev/null and b/server/alert/hourly-forecast.mp3 differ diff --git a/server/alert/hourly-graph.mp3 b/server/alert/hourly-graph.mp3 new file mode 100644 index 0000000..222463b Binary files /dev/null and b/server/alert/hourly-graph.mp3 differ diff --git a/server/alert/local-radar.mp3 b/server/alert/local-radar.mp3 new file mode 100644 index 0000000..2f77138 Binary files /dev/null and b/server/alert/local-radar.mp3 differ diff --git a/server/alert/regional-observations.mp3 b/server/alert/regional-observations.mp3 new file mode 100644 index 0000000..d4ff295 Binary files /dev/null and b/server/alert/regional-observations.mp3 differ diff --git a/server/alert/travel-forecast.mp3 b/server/alert/travel-forecast.mp3 new file mode 100644 index 0000000..c62c644 Binary files /dev/null and b/server/alert/travel-forecast.mp3 differ diff --git a/server/scripts/modules/media.mjs b/server/scripts/modules/media.mjs index 51c53f9..986ce7f 100644 --- a/server/scripts/modules/media.mjs +++ b/server/scripts/modules/media.mjs @@ -59,6 +59,8 @@ document.addEventListener('DOMContentLoaded', () => { if (mediaVolume.value !== clampMediaVolume(mediaVolume.value)) { mediaVolume.value = clampMediaVolume(mediaVolume.value); } + + // Screen audio setting is managed via localStorage and checked directly in playScreenAudio() }); const unlockAudio = () => { @@ -342,6 +344,7 @@ const startAlertTone = async () => { alertTonePending = true; return; } + stopScreenAudio(); // Stop screen audio when alert plays initializeAlertTonePlayer(); try { alertTonePending = true; @@ -370,6 +373,82 @@ const stopAlertTone = () => { const playAlertTone = () => startAlertTone(); +// Screen audio constants and variables +const SCREEN_AUDIO_DUCK_VOLUME = 0.10; // 10% ducking (vs 5% for alerts) +const screenAudioMap = { + 'radar': 'local-radar.mp3', + 'regional-forecast': 'regional-observations.mp3', + 'travel': 'travel-forecast.mp3', + 'hourly-graph': 'hourly-graph.mp3', + 'hourly': 'hourly-forecast.mp3', + 'current-weather': 'current-conditions.mp3', +}; + +let screenAudioPlayer = null; + +// Helper function to check if screen audio is enabled (always reads from localStorage) +const isScreenAudioEnabled = () => { + const saved = window.localStorage.getItem('screenAudioEnabled'); + return saved !== null ? saved === 'true' : true; // Default: enabled +}; + +// Play screen audio +const playScreenAudio = async (screenId) => { + // Always check localStorage to ensure setting is current + if (!isScreenAudioEnabled()) return; + + const fileName = screenAudioMap[screenId]; + if (!fileName) return; + + // Stop any existing screen audio first + stopScreenAudio(); + + // Don't play if alert tone is active + if (alertToneActive || alertTonePending) return; + + // Duck background music to 10% + if (player && !player.paused) { + player.volume = SCREEN_AUDIO_DUCK_VOLUME; + } + + // Create and play audio + screenAudioPlayer = new Audio(withBasePath(`alert/${fileName}`)); + screenAudioPlayer.type = 'audio/mpeg'; + + screenAudioPlayer.addEventListener('ended', () => { + screenAudioPlayer = null; + // Only restore if alert isn't playing + if (!alertToneActive && !alertTonePending) { + restoreMediaAfterAlert(); + } + }); + + screenAudioPlayer.addEventListener('error', () => { + screenAudioPlayer = null; + if (!alertToneActive && !alertTonePending) { + restoreMediaAfterAlert(); + } + }); + + try { + await screenAudioPlayer.play(); + } catch (e) { + screenAudioPlayer = null; + if (!alertToneActive && !alertTonePending) { + restoreMediaAfterAlert(); + } + } +}; + +// Stop screen audio immediately +const stopScreenAudio = () => { + if (screenAudioPlayer) { + screenAudioPlayer.pause(); + screenAudioPlayer.currentTime = 0; + screenAudioPlayer = null; + } +}; + const playerCanPlay = async () => { // check to make sure they user still wants music (protect against slow loading music) if (!mediaPlaying.value) return; @@ -402,4 +481,6 @@ export { handleClick, playAlertTone, stopAlertTone, + playScreenAudio, + stopScreenAudio, }; diff --git a/server/scripts/modules/navigation.mjs b/server/scripts/modules/navigation.mjs index 2b0ee63..4a5d27a 100644 --- a/server/scripts/modules/navigation.mjs +++ b/server/scripts/modules/navigation.mjs @@ -385,10 +385,18 @@ const handleNavButton = (button) => { break; case 'next': setPlaying(false); + // Stop screen audio immediately when navigating + import('./media.mjs').then((media) => { + media.stopScreenAudio(); + }); navTo(msg.command.nextFrame); break; case 'previous': setPlaying(false); + // Stop screen audio immediately when navigating + import('./media.mjs').then((media) => { + media.stopScreenAudio(); + }); navTo(msg.command.previousFrame); break; case 'menu': diff --git a/server/scripts/modules/settings.mjs b/server/scripts/modules/settings.mjs index 34a0300..aea7e0c 100644 --- a/server/scripts/modules/settings.mjs +++ b/server/scripts/modules/settings.mjs @@ -231,6 +231,32 @@ document.addEventListener('DOMContentLoaded', () => { settingsSection.innerHTML = ''; settingsSection.append(...settingHtml); + // Add screen audio toggle checkbox + const screenAudioContainer = document.createElement('div'); + screenAudioContainer.className = 'info'; + const screenAudioLabel = document.createElement('label'); + screenAudioLabel.htmlFor = 'screen-audio-checkbox'; + const screenAudioCheckbox = document.createElement('input'); + screenAudioCheckbox.type = 'checkbox'; + screenAudioCheckbox.id = 'screen-audio-checkbox'; + // Load saved preference or default to enabled + const savedScreenAudio = window.localStorage.getItem('screenAudioEnabled'); + screenAudioCheckbox.checked = savedScreenAudio !== null ? savedScreenAudio === 'true' : true; + screenAudioCheckbox.addEventListener('change', () => { + window.localStorage.setItem('screenAudioEnabled', screenAudioCheckbox.checked); + // Import media module and update setting + import('./media.mjs').then((media) => { + // Media module will read the localStorage value + if (!screenAudioCheckbox.checked) { + media.stopScreenAudio(); + } + }); + }); + screenAudioLabel.appendChild(screenAudioCheckbox); + screenAudioLabel.appendChild(document.createTextNode(' Play screen audio')); + screenAudioContainer.appendChild(screenAudioLabel); + settingsSection.appendChild(screenAudioContainer); + // update visibility on some settings const modeSelect = document.getElementById('settings-scanLineMode-label'); const { value } = settings.scanLines; diff --git a/server/scripts/modules/weatherdisplay.mjs b/server/scripts/modules/weatherdisplay.mjs index a663bac..53d682e 100644 --- a/server/scripts/modules/weatherdisplay.mjs +++ b/server/scripts/modules/weatherdisplay.mjs @@ -219,8 +219,19 @@ class WeatherDisplay { this.startNavCount(); + // Check if display was already active before showing + const wasActive = this.active; + this.elem.classList.add('show'); document.querySelector('#divTwc').classList.add(this.elemId); + + // Play screen-specific audio only if display was not already active + // This prevents audio restart on frame changes (e.g., Local Radar animation) + if (!wasActive) { + import('./media.mjs').then((media) => { + media.playScreenAudio(this.elemId); + }); + } } hideCanvas() { @@ -228,6 +239,11 @@ class WeatherDisplay { this.elem.classList.remove('show'); // used to change backgrounds for widescreen document.querySelector('#divTwc').classList.remove(this.elemId); + + // Stop screen audio when leaving + import('./media.mjs').then((media) => { + media.stopScreenAudio(); + }); } get active() {