/** * TaskMate Config Flow Sound Preview * Plays sound preview when the completion_sound dropdown value changes * in the config flow (add/edit chore screens). */ (function() { 'use strict'; // Debug logging gate. Set window.__taskmate_config_sounds_debug = true in the // browser console to enable the chatty console.debug/warn output. const debugLog = (...args) => { if (window.__taskmate_config_sounds_debug) console.debug(...args); }; const debugWarn = (...args) => { if (window.__taskmate_config_sounds_debug) console.warn(...args); }; // Audio context for sound preview (lazy initialized) let audioContext = null; /** * Get or create AudioContext */ function getAudioContext() { if (!audioContext) { audioContext = new (window.AudioContext || window.webkitAudioContext)(); } if (audioContext.state === 'suspended') { audioContext.resume(); } return audioContext; } // ============== SOUND IMPLEMENTATIONS ============== // These are copies from taskmate-child-card.js to keep this file standalone function playCoinSound(ctx, startTime) { const masterGain = ctx.createGain(); masterGain.connect(ctx.destination); masterGain.gain.value = 0.3; const osc1 = ctx.createOscillator(); const gain1 = ctx.createGain(); osc1.connect(gain1); gain1.connect(masterGain); osc1.frequency.value = 1318.5; osc1.type = 'square'; gain1.gain.setValueAtTime(0.5, startTime); gain1.gain.exponentialRampToValueAtTime(0.01, startTime + 0.1); osc1.start(startTime); osc1.stop(startTime + 0.1); const osc2 = ctx.createOscillator(); const gain2 = ctx.createGain(); osc2.connect(gain2); gain2.connect(masterGain); osc2.frequency.value = 1975.5; osc2.type = 'square'; gain2.gain.setValueAtTime(0.5, startTime + 0.08); gain2.gain.exponentialRampToValueAtTime(0.01, startTime + 0.25); osc2.start(startTime + 0.08); osc2.stop(startTime + 0.25); } function playLevelUpSound(ctx, startTime) { const masterGain = ctx.createGain(); masterGain.connect(ctx.destination); masterGain.gain.value = 0.25; const notes = [523.25, 659.25, 783.99, 1046.5]; const duration = 0.12; notes.forEach((freq, i) => { const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(masterGain); osc.frequency.value = freq; osc.type = 'square'; const noteStart = startTime + i * duration; gain.gain.setValueAtTime(0.6, noteStart); gain.gain.exponentialRampToValueAtTime(0.01, noteStart + duration + 0.1); osc.start(noteStart); osc.stop(noteStart + duration + 0.15); }); const chordNotes = [523.25, 659.25, 783.99]; const chordStart = startTime + notes.length * duration; chordNotes.forEach((freq) => { const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(masterGain); osc.frequency.value = freq; osc.type = 'triangle'; gain.gain.setValueAtTime(0.3, chordStart); gain.gain.exponentialRampToValueAtTime(0.01, chordStart + 0.5); osc.start(chordStart); osc.stop(chordStart + 0.55); }); } function playFanfareSound(ctx, startTime) { const masterGain = ctx.createGain(); masterGain.connect(ctx.destination); masterGain.gain.value = 0.2; const pattern = [ { freq: 392.00, duration: 0.1, delay: 0 }, { freq: 392.00, duration: 0.1, delay: 0.12 }, { freq: 392.00, duration: 0.15, delay: 0.24 }, { freq: 329.63, duration: 0.15, delay: 0.42 }, { freq: 392.00, duration: 0.15, delay: 0.6 }, { freq: 523.25, duration: 0.4, delay: 0.78 }, ]; pattern.forEach(({ freq, duration, delay }) => { const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(masterGain); osc.frequency.value = freq; osc.type = 'sawtooth'; const noteStart = startTime + delay; gain.gain.setValueAtTime(0.5, noteStart); gain.gain.setValueAtTime(0.5, noteStart + duration * 0.8); gain.gain.exponentialRampToValueAtTime(0.01, noteStart + duration); osc.start(noteStart); osc.stop(noteStart + duration + 0.05); }); } function playChimeSound(ctx, startTime) { const masterGain = ctx.createGain(); masterGain.connect(ctx.destination); masterGain.gain.value = 0.3; const fundamental = 880; const harmonics = [1, 2, 3, 4.2]; harmonics.forEach((harmonic, i) => { const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(masterGain); osc.frequency.value = fundamental * harmonic; osc.type = 'sine'; const amplitude = 0.5 / (i + 1); const decayTime = 0.8 / (i + 1); gain.gain.setValueAtTime(amplitude, startTime); gain.gain.exponentialRampToValueAtTime(0.001, startTime + decayTime); osc.start(startTime); osc.stop(startTime + decayTime + 0.1); }); } function playPowerUpSound(ctx, startTime) { const masterGain = ctx.createGain(); masterGain.connect(ctx.destination); masterGain.gain.value = 0.25; const osc1 = ctx.createOscillator(); const gain1 = ctx.createGain(); osc1.connect(gain1); gain1.connect(masterGain); osc1.type = 'sawtooth'; osc1.frequency.setValueAtTime(200, startTime); osc1.frequency.exponentialRampToValueAtTime(1200, startTime + 0.3); gain1.gain.setValueAtTime(0.4, startTime); gain1.gain.exponentialRampToValueAtTime(0.01, startTime + 0.35); osc1.start(startTime); osc1.stop(startTime + 0.4); const sparkleNotes = [1318.5, 1567.98, 1975.5]; sparkleNotes.forEach((freq, i) => { const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(masterGain); osc.frequency.value = freq; osc.type = 'sine'; const noteStart = startTime + 0.25 + i * 0.05; gain.gain.setValueAtTime(0.3, noteStart); gain.gain.exponentialRampToValueAtTime(0.01, noteStart + 0.2); osc.start(noteStart); osc.stop(noteStart + 0.25); }); } function playUndoSound(ctx, startTime) { const masterGain = ctx.createGain(); masterGain.connect(ctx.destination); masterGain.gain.value = 0.25; const osc1 = ctx.createOscillator(); const gain1 = ctx.createGain(); osc1.connect(gain1); gain1.connect(masterGain); osc1.type = 'triangle'; osc1.frequency.setValueAtTime(311.13, startTime); osc1.frequency.exponentialRampToValueAtTime(233.08, startTime + 0.25); gain1.gain.setValueAtTime(0.6, startTime); gain1.gain.exponentialRampToValueAtTime(0.3, startTime + 0.2); gain1.gain.exponentialRampToValueAtTime(0.01, startTime + 0.3); osc1.start(startTime); osc1.stop(startTime + 0.35); const osc2 = ctx.createOscillator(); const gain2 = ctx.createGain(); osc2.connect(gain2); gain2.connect(masterGain); osc2.type = 'triangle'; osc2.frequency.setValueAtTime(233.08, startTime + 0.3); osc2.frequency.exponentialRampToValueAtTime(155.56, startTime + 0.7); gain2.gain.setValueAtTime(0.5, startTime + 0.3); gain2.gain.exponentialRampToValueAtTime(0.25, startTime + 0.55); gain2.gain.exponentialRampToValueAtTime(0.01, startTime + 0.75); osc2.start(startTime + 0.3); osc2.stop(startTime + 0.8); const osc3 = ctx.createOscillator(); const gain3 = ctx.createGain(); osc3.connect(gain3); gain3.connect(masterGain); osc3.type = 'sine'; osc3.frequency.setValueAtTime(116.54, startTime + 0.5); gain3.gain.setValueAtTime(0.15, startTime + 0.5); gain3.gain.exponentialRampToValueAtTime(0.01, startTime + 0.8); osc3.start(startTime + 0.5); osc3.stop(startTime + 0.85); } function playFartSound(ctx, startTime) { const masterGain = ctx.createGain(); masterGain.connect(ctx.destination); masterGain.gain.value = 0.9; const noiseLength = ctx.sampleRate * 0.5; const noiseBuffer = ctx.createBuffer(1, noiseLength, ctx.sampleRate); const noiseData = noiseBuffer.getChannelData(0); let lastOut = 0; for (let i = 0; i < noiseLength; i++) { const white = Math.random() * 2 - 1; noiseData[i] = (lastOut + (0.04 * white)) / 1.04; lastOut = noiseData[i]; noiseData[i] *= 8; } const noiseSource = ctx.createBufferSource(); noiseSource.buffer = noiseBuffer; const filter = ctx.createBiquadFilter(); filter.type = 'bandpass'; filter.frequency.setValueAtTime(200, startTime); filter.frequency.exponentialRampToValueAtTime(80, startTime + 0.4); filter.Q.value = 2; const noiseGain = ctx.createGain(); noiseGain.gain.setValueAtTime(0, startTime); noiseGain.gain.linearRampToValueAtTime(1.0, startTime + 0.01); noiseGain.gain.setValueAtTime(1.0, startTime + 0.15); noiseGain.gain.exponentialRampToValueAtTime(0.01, startTime + 0.45); noiseSource.connect(filter); filter.connect(noiseGain); noiseGain.connect(masterGain); noiseSource.start(startTime); noiseSource.stop(startTime + 0.5); const bass = ctx.createOscillator(); const bassGain = ctx.createGain(); bass.type = 'sawtooth'; bass.frequency.setValueAtTime(80, startTime); bass.frequency.exponentialRampToValueAtTime(40, startTime + 0.4); bassGain.gain.setValueAtTime(0, startTime); bassGain.gain.linearRampToValueAtTime(0.8, startTime + 0.02); bassGain.gain.exponentialRampToValueAtTime(0.01, startTime + 0.45); bass.connect(bassGain); bassGain.connect(masterGain); bass.start(startTime); bass.stop(startTime + 0.5); const flap = ctx.createOscillator(); const flapGain = ctx.createGain(); flap.type = 'square'; flap.frequency.setValueAtTime(25, startTime); flap.frequency.exponentialRampToValueAtTime(12, startTime + 0.4); flapGain.gain.setValueAtTime(0.6, startTime); flapGain.gain.exponentialRampToValueAtTime(0.01, startTime + 0.4); flap.connect(flapGain); flapGain.connect(masterGain); flap.start(startTime); flap.stop(startTime + 0.45); const sub = ctx.createOscillator(); const subGain = ctx.createGain(); sub.type = 'sine'; sub.frequency.setValueAtTime(50, startTime); sub.frequency.exponentialRampToValueAtTime(25, startTime + 0.3); subGain.gain.setValueAtTime(0.7, startTime); subGain.gain.exponentialRampToValueAtTime(0.01, startTime + 0.35); sub.connect(subGain); subGain.connect(masterGain); sub.start(startTime); sub.stop(startTime + 0.4); } function playFartLongSound(ctx, startTime) { const masterGain = ctx.createGain(); masterGain.connect(ctx.destination); masterGain.gain.value = 0.85; const noiseLength = ctx.sampleRate * 2.0; const noiseBuffer = ctx.createBuffer(1, noiseLength, ctx.sampleRate); const noiseData = noiseBuffer.getChannelData(0); let lastOut = 0; for (let i = 0; i < noiseLength; i++) { const white = Math.random() * 2 - 1; noiseData[i] = (lastOut + (0.05 * white)) / 1.05; lastOut = noiseData[i]; noiseData[i] *= 7; } const noiseSource = ctx.createBufferSource(); noiseSource.buffer = noiseBuffer; const filter = ctx.createBiquadFilter(); filter.type = 'bandpass'; filter.frequency.setValueAtTime(250, startTime); filter.frequency.linearRampToValueAtTime(180, startTime + 0.3); filter.frequency.linearRampToValueAtTime(280, startTime + 0.5); filter.frequency.linearRampToValueAtTime(150, startTime + 0.9); filter.frequency.linearRampToValueAtTime(220, startTime + 1.2); filter.frequency.exponentialRampToValueAtTime(60, startTime + 1.8); filter.Q.value = 3; const noiseGain = ctx.createGain(); noiseGain.gain.setValueAtTime(0, startTime); noiseGain.gain.linearRampToValueAtTime(1.0, startTime + 0.02); noiseGain.gain.linearRampToValueAtTime(0.7, startTime + 0.4); noiseGain.gain.linearRampToValueAtTime(1.0, startTime + 0.6); noiseGain.gain.linearRampToValueAtTime(0.5, startTime + 1.0); noiseGain.gain.linearRampToValueAtTime(0.9, startTime + 1.3); noiseGain.gain.exponentialRampToValueAtTime(0.01, startTime + 1.9); noiseSource.connect(filter); filter.connect(noiseGain); noiseGain.connect(masterGain); noiseSource.start(startTime); noiseSource.stop(startTime + 2.0); const bass = ctx.createOscillator(); const bassGain = ctx.createGain(); bass.type = 'sawtooth'; bass.frequency.setValueAtTime(90, startTime); bass.frequency.linearRampToValueAtTime(70, startTime + 0.5); bass.frequency.linearRampToValueAtTime(100, startTime + 0.8); bass.frequency.linearRampToValueAtTime(50, startTime + 1.3); bass.frequency.exponentialRampToValueAtTime(30, startTime + 1.8); bassGain.gain.setValueAtTime(0.8, startTime); bassGain.gain.exponentialRampToValueAtTime(0.01, startTime + 1.85); bass.connect(bassGain); bassGain.connect(masterGain); bass.start(startTime); bass.stop(startTime + 1.9); const flutter = ctx.createOscillator(); const flutterGain = ctx.createGain(); flutter.type = 'square'; flutter.frequency.setValueAtTime(35, startTime); flutter.frequency.linearRampToValueAtTime(20, startTime + 0.8); flutter.frequency.linearRampToValueAtTime(28, startTime + 1.0); flutter.frequency.exponentialRampToValueAtTime(6, startTime + 1.8); flutterGain.gain.setValueAtTime(0.7, startTime); flutterGain.gain.exponentialRampToValueAtTime(0.01, startTime + 1.8); flutter.connect(flutterGain); flutterGain.connect(masterGain); flutter.start(startTime); flutter.stop(startTime + 1.85); const sub = ctx.createOscillator(); const subGain = ctx.createGain(); sub.type = 'sine'; sub.frequency.setValueAtTime(45, startTime); sub.frequency.exponentialRampToValueAtTime(20, startTime + 1.7); subGain.gain.setValueAtTime(0.6, startTime); subGain.gain.exponentialRampToValueAtTime(0.01, startTime + 1.75); sub.connect(subGain); subGain.connect(masterGain); sub.start(startTime); sub.stop(startTime + 1.8); const squeakTimes = [0.5, 1.2]; squeakTimes.forEach((delay) => { const squeak = ctx.createOscillator(); const squeakGain = ctx.createGain(); squeak.type = 'triangle'; squeak.frequency.setValueAtTime(180, startTime + delay); squeak.frequency.exponentialRampToValueAtTime(120, startTime + delay + 0.15); squeakGain.gain.setValueAtTime(0.4, startTime + delay); squeakGain.gain.exponentialRampToValueAtTime(0.01, startTime + delay + 0.15); squeak.connect(squeakGain); squeakGain.connect(masterGain); squeak.start(startTime + delay); squeak.stop(startTime + delay + 0.2); }); } /** * Play a sound by name */ function playSound(soundName) { if (!soundName || soundName === 'none') { debugLog('[TaskMate Config] No sound to play'); return; } try { const ctx = getAudioContext(); const now = ctx.currentTime; switch (soundName) { case 'coin': playCoinSound(ctx, now); break; case 'levelup': playLevelUpSound(ctx, now); break; case 'fanfare': playFanfareSound(ctx, now); break; case 'chime': playChimeSound(ctx, now); break; case 'powerup': playPowerUpSound(ctx, now); break; case 'undo': playUndoSound(ctx, now); break; case 'fart': playFartSound(ctx, now); break; case 'fart_long': playFartLongSound(ctx, now); break; default: debugWarn(`[TaskMate Config] Unknown sound: ${soundName}`); playCoinSound(ctx, now); } } catch (e) { debugWarn('[TaskMate Config] Error playing sound:', e); } } // Valid sound names const VALID_SOUNDS = ['coin', 'levelup', 'fanfare', 'chime', 'powerup', 'undo', 'fart', 'fart_long', 'none']; /** * Map display text to sound value */ function textToSoundValue(text) { if (!text) return null; const normalized = text.toLowerCase().trim().replace(/\s+/g, '_'); const map = { 'no_sound': 'none', 'no sound': 'none', 'coin': 'coin', 'levelup': 'levelup', 'level_up': 'levelup', 'fanfare': 'fanfare', 'chime': 'chime', 'powerup': 'powerup', 'power_up': 'powerup', 'undo': 'undo', 'fart': 'fart', 'fart_long': 'fart_long', }; return map[normalized] || (VALID_SOUNDS.includes(normalized) ? normalized : null); } /** * Attach change listener to a select element */ function attachSoundChangeListener(element) { if (element.dataset.taskmateSoundListener) return; element.dataset.taskmateSoundListener = 'true'; // Listen for various change events const handleChange = (e) => { const value = e.target?.value || e.detail?.value; const soundValue = textToSoundValue(value) || value; debugLog('[TaskMate Config] Sound changed to:', soundValue); if (soundValue && soundValue !== 'none' && VALID_SOUNDS.includes(soundValue)) { playSound(soundValue); } }; element.addEventListener('change', handleChange); element.addEventListener('value-changed', handleChange); element.addEventListener('selected', handleChange); element.addEventListener('click', (e) => { // For dropdown items being clicked const item = e.target.closest('mwc-list-item, ha-list-item, [role="option"]'); if (item) { const value = item.value || item.getAttribute('value') || item.textContent; const soundValue = textToSoundValue(value); if (soundValue && soundValue !== 'none') { setTimeout(() => playSound(soundValue), 50); } } }); debugLog('[TaskMate Config] Attached sound change listener to element'); } /** * Find sound selectors and attach listeners */ function findAndEnhanceSoundSelectors() { // Look for any select/dropdown that might be for sounds const selectors = document.querySelectorAll('ha-select, mwc-select, ha-combo-box, select'); selectors.forEach(selector => { // Check if this selector has sound-related options const options = selector.querySelectorAll('mwc-list-item, ha-list-item, option, [role="option"]'); let isSoundSelector = false; options.forEach(opt => { const text = (opt.value || opt.getAttribute('value') || opt.textContent || '').toLowerCase(); if (VALID_SOUNDS.some(s => text.includes(s))) { isSoundSelector = true; } }); // Also check the selector's current value const currentValue = (selector.value || '').toLowerCase(); if (VALID_SOUNDS.some(s => currentValue.includes(s))) { isSoundSelector = true; } if (isSoundSelector) { attachSoundChangeListener(selector); // Also attach to inner elements if present const innerSelect = selector.shadowRoot?.querySelector('select, mwc-menu, mwc-list'); if (innerSelect) { attachSoundChangeListener(innerSelect); } } }); // Also look for ha-selector elements (HA's custom form selectors) document.querySelectorAll('ha-selector').forEach(haSelector => { const innerSelect = haSelector.shadowRoot?.querySelector('ha-select, mwc-select, select') || haSelector.querySelector('ha-select, mwc-select, select'); if (innerSelect) { const options = innerSelect.querySelectorAll('mwc-list-item, [role="option"]'); let isSoundSelector = false; options.forEach(opt => { const text = (opt.value || opt.textContent || '').toLowerCase(); if (VALID_SOUNDS.some(s => text.includes(s))) { isSoundSelector = true; } }); if (isSoundSelector) { attachSoundChangeListener(innerSelect); } } }); } /** * Watch for DOM changes and enhance new sound selectors */ function startObserver() { const observer = new MutationObserver((mutations) => { let shouldScan = false; mutations.forEach((mutation) => { if (mutation.addedNodes.length > 0) { shouldScan = true; } }); if (shouldScan) { // Debounce scanning clearTimeout(window._taskmateScanTimeout); window._taskmateScanTimeout = setTimeout(findAndEnhanceSoundSelectors, 100); } }); observer.observe(document.body, { childList: true, subtree: true, }); window._taskmateSoundObserver = observer; debugLog('[TaskMate Config] Sound change observer started'); } // Initialize function init() { if (window._taskmateConfigSoundsInit) return; window._taskmateConfigSoundsInit = true; findAndEnhanceSoundSelectors(); startObserver(); // Slow periodic scan as a safety net for dynamically loaded content the // MutationObserver doesn't catch (e.g. dialogs rendered outside body's light DOM) window._taskmateConfigSoundsPoll = setInterval(findAndEnhanceSoundSelectors, 10000); const cleanup = () => { if (window._taskmateConfigSoundsPoll) { clearInterval(window._taskmateConfigSoundsPoll); window._taskmateConfigSoundsPoll = null; } if (window._taskmateScanTimeout) { clearTimeout(window._taskmateScanTimeout); window._taskmateScanTimeout = null; } if (window._taskmateSoundObserver) { window._taskmateSoundObserver.disconnect(); window._taskmateSoundObserver = null; } }; // pagehide covers SPA navigation in HA; beforeunload covers full reloads. window.addEventListener('pagehide', cleanup, { once: true }); window.addEventListener('beforeunload', cleanup, { once: true }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } window.addEventListener('load', () => setTimeout(init, 500)); // Listen for taskmate_preview_sound events fired by the preview_sound service document.addEventListener('taskmate_preview_sound', (e) => { const sound = e.detail?.sound || e.detail?.data?.sound; if (sound) playSound(sound); }); // Also listen via window for HA event bus forwarding window.addEventListener('taskmate_preview_sound', (e) => { const sound = e.detail?.sound || e.detail?.data?.sound; if (sound) playSound(sound); }); console.info('[TaskMate] Config sound preview module loaded - sounds will play on selection change'); })();