Add optional sound effects for state changes
- Warning: soft double-beep - Critical: urgent descending tone - Notify: gentle ping - Recovery: happy ascending chirp Enable with ?sound=on or tap screen to activate. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
92
index.html
92
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;
|
||||
|
||||
Reference in New Issue
Block a user