diff --git a/index.html b/index.html index 2f17c90..b088db8 100644 --- a/index.html +++ b/index.html @@ -199,6 +199,95 @@ const messageEl = document.getElementById('message'); const POLL_INTERVAL = 2000; + // Sound system + let audioCtx = null; + let soundEnabled = new URLSearchParams(window.location.search).get('sound') === 'on'; + let lastState = null; + + function initAudio() { + if (!audioCtx) { + audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + } + if (audioCtx.state === 'suspended') { + audioCtx.resume(); + } + } + + function playTone(frequency, duration, type = 'sine', volume = 0.1) { + if (!soundEnabled || !audioCtx) return; + const osc = audioCtx.createOscillator(); + const gain = audioCtx.createGain(); + osc.type = type; + osc.frequency.value = frequency; + gain.gain.value = volume; + gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + duration); + osc.connect(gain); + gain.connect(audioCtx.destination); + osc.start(); + osc.stop(audioCtx.currentTime + duration); + } + + function playWarningSound() { + // Soft double-beep + playTone(440, 0.15); + setTimeout(() => playTone(440, 0.15), 180); + } + + function playCriticalSound() { + // Urgent descending tone + playTone(600, 0.2); + setTimeout(() => playTone(400, 0.3), 220); + } + + function playNotifySound() { + // Gentle ping + playTone(880, 0.1, 'sine', 0.08); + } + + function playRecoverySound() { + // Happy ascending chirp + playTone(523, 0.1); + setTimeout(() => playTone(659, 0.1), 100); + setTimeout(() => playTone(784, 0.15), 200); + } + + function handleStateChange(newState, newEmote) { + if (!lastState) { + lastState = newState; + return; + } + + // State transitions that trigger sounds + if (newState !== lastState) { + if (newState === 'critical') { + playCriticalSound(); + } else if (newState === 'warning') { + playWarningSound(); + } else if (newState === 'notify') { + playNotifySound(); + } else if (newState === 'optimal' && lastState !== 'optimal' && lastState !== 'sleeping') { + // Recovery - also check for celebration emote + playRecoverySound(); + } + lastState = newState; + } + } + + // Enable sound on first tap (browser autoplay policy) + document.body.addEventListener('click', () => { + if (!soundEnabled) { + soundEnabled = true; + initAudio(); + // Brief confirmation chirp + playTone(660, 0.08, 'sine', 0.05); + } + }, { once: false }); + + // Also init if ?sound=on + if (soundEnabled) { + document.addEventListener('DOMContentLoaded', initAudio); + } + async function fetchStatus() { try { const response = await fetch('/status'); @@ -216,6 +305,9 @@ } function updateDisplay(data) { + // Check for state changes and play sounds + handleStateChange(data.current_state, data.active_emote); + emoteEl.textContent = data.active_emote; emoteEl.style.color = data.color; messageEl.style.color = data.color;