Files
HomeAssistantVS/custom_components/taskmate/www/taskmate-config-sounds.js
T
2026-07-08 10:43:39 -04:00

648 lines
22 KiB
JavaScript

/**
* 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');
})();