/** * AudioManager Module * Manages loading and playback of non-TTS audio effects triggered by tags. */ export class AudioManager { constructor() { this.sounds = new Map(); this.currentAudio = null; this.currentLoop = null; } /** * Load a sound file * @param {string} id - The identifier for the sound * @param {string} url - The URL of the sound file * @returns {Promise} A promise that resolves when the sound is loaded */ loadSound(id, url) { return new Promise((resolve, reject) => { const audio = new Audio(url); audio.addEventListener('canplaythrough', () => { this.sounds.set(id, audio); resolve(audio); }, { once: true }); audio.addEventListener('error', (error) => { reject(error); }); audio.load(); }); } /** * Play a sound * @param {string} id - The identifier for the sound * @param {boolean} loop - Whether to loop the sound * @returns {HTMLAudioElement|null} The audio element or null if not found */ playSound(id, loop = false) { const audio = this.sounds.get(id); if (!audio) { console.warn(`Sound with id "${id}" not found.`); return null; } if (loop) { if (this.currentLoop) { this.currentLoop.pause(); this.currentLoop.currentTime = 0; } audio.loop = true; this.currentLoop = audio; } else { if (this.currentAudio) { this.currentAudio.pause(); this.currentAudio.currentTime = 0; } this.currentAudio = audio; } audio.play().catch(error => { console.error('Error playing audio:', error); }); return audio; } /** * Play a sound from a URL directly (without preloading) * @param {string} url - The URL of the sound file * @param {boolean} loop - Whether to loop the sound * @returns {HTMLAudioElement} The audio element */ playSoundFromUrl(url, loop = false) { if (loop) { if (this.currentLoop) { this.currentLoop.pause(); this.currentLoop.removeAttribute('src'); this.currentLoop.load(); } this.currentLoop = new Audio(url); this.currentLoop.loop = true; this.currentLoop.play().catch(error => { console.error('Error playing audio loop:', error); }); return this.currentLoop; } else { if (this.currentAudio) { this.currentAudio.pause(); this.currentAudio.removeAttribute('src'); this.currentAudio.load(); } this.currentAudio = new Audio(url); this.currentAudio.play().catch(error => { console.error('Error playing audio:', error); }); return this.currentAudio; } } /** * Stop a specific sound * @param {string} id - The identifier for the sound */ stopSound(id) { const audio = this.sounds.get(id); if (audio) { audio.pause(); audio.currentTime = 0; } } /** * Stop all sounds */ stopAllSounds() { if (this.currentAudio) { this.currentAudio.pause(); this.currentAudio.currentTime = 0; this.currentAudio = null; } if (this.currentLoop) { this.currentLoop.pause(); this.currentLoop.currentTime = 0; this.currentLoop = null; } this.sounds.forEach(audio => { audio.pause(); audio.currentTime = 0; }); } /** * Set the volume for all sounds * @param {number} volume - The volume level (0.0 to 1.0) */ setVolume(volume) { this.sounds.forEach(audio => { audio.volume = volume; }); if (this.currentAudio) { this.currentAudio.volume = volume; } if (this.currentLoop) { this.currentLoop.volume = volume; } } /** * Check if a sound is currently playing * @param {string} id - The identifier for the sound * @returns {boolean} Whether the sound is playing */ isPlaying(id) { const audio = this.sounds.get(id); return audio ? !audio.paused : false; } /** * Fade out the current audio * @param {number} duration - The duration of the fade in milliseconds * @returns {Promise} A promise that resolves when the fade is complete */ fadeOutCurrentAudio(duration = 1000) { return new Promise((resolve) => { if (!this.currentAudio || this.currentAudio.paused) { resolve(); return; } const audio = this.currentAudio; const initialVolume = audio.volume; const volumeStep = initialVolume / (duration / 50); let currentVolume = initialVolume; const fadeInterval = setInterval(() => { currentVolume -= volumeStep; if (currentVolume <= 0) { clearInterval(fadeInterval); audio.pause(); audio.currentTime = 0; audio.volume = initialVolume; // Reset volume for future use resolve(); } else { audio.volume = currentVolume; } }, 50); }); } }