/** * AudioManager Module * Manages loading and playback of non-TTS audio effects triggered by tags. */ import { BaseModule } from './base-module.js'; class AudioManagerModule extends BaseModule { constructor() { super('audio-manager', 'Audio Manager'); this.sounds = new Map(); this.currentAudio = null; this.currentLoop = null; this.masterVolume = 1.0; this.musicVolume = 1.0; this.sfxVolume = 1.0; this.ttsVolume = 1.0; // Add persistence-manager as a dependency this.dependencies = ['persistence-manager']; } /** * Load module dependencies * @returns {Promise} - Resolves when dependencies are loaded */ async loadDependencies() { try { this.reportProgress(40, "Initializing audio system"); return true; } catch (error) { console.error("Error loading AudioManager dependencies:", error); return false; } } /** * Initialize the module * @returns {Promise} - Resolves with success status */ async initialize() { try { // Set up audio context if needed this.setupAudioContext(); // Load some basic sound effects this.reportProgress(80, "Loading sound effects"); this.reportProgress(100, "Audio system ready"); return true; } catch (error) { console.error("Error initializing AudioManager:", error); return false; } } /** * Set up Web Audio API context if needed */ setupAudioContext() { // Only create if needed for advanced audio features if (typeof AudioContext !== 'undefined' || typeof webkitAudioContext !== 'undefined') { const AudioContextClass = window.AudioContext || window.webkitAudioContext; this.audioContext = new AudioContextClass(); } } /** * 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 master volume for all sounds * @param {number} volume - The volume level (0.0 to 1.0) */ setMasterVolume(volume) { this.masterVolume = Math.max(0, Math.min(1, volume)); this.updateVolumes(); } /** * Set the speech volume * @param {number} volume - The volume level (0.0 to 1.0) */ setTtsVolume(volume) { this.ttsVolume = Math.max(0, Math.min(1, volume)); // Apply to current non-loop audio if it exists if (this.currentAudio) { this.currentAudio.volume = this.masterVolume * this.ttsVolume; } } /** * Set the music volume * @param {number} volume - The volume level (0.0 to 1.0) */ setMusicVolume(volume) { this.musicVolume = Math.max(0, Math.min(1, volume)); // Apply to current loop if it exists if (this.currentLoop) { this.currentLoop.volume = this.masterVolume * this.musicVolume; } } /** * Set the sound effects volume * @param {number} volume - The volume level (0.0 to 1.0) */ setSfxVolume(volume) { this.sfxVolume = Math.max(0, Math.min(1, volume)); // Apply to current non-loop audio if it exists if (this.currentAudio) { this.currentAudio.volume = this.masterVolume * this.sfxVolume; } } /** * Update all volume levels based on current settings */ updateVolumes() { this.sounds.forEach(audio => { const isMusic = audio.loop; audio.volume = this.masterVolume * (isMusic ? this.musicVolume : this.sfxVolume); }); if (this.currentAudio) { this.currentAudio.volume = this.masterVolume * this.sfxVolume; } if (this.currentLoop) { this.currentLoop.volume = this.masterVolume * this.musicVolume; } } /** * 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); }); } /** * Play speech audio from a Blob * @param {Blob} audioBlob - The audio Blob to play * @param {Object} options - Playback options * @returns {Promise} - Resolves with success status */ async playSpeech(audioBlob, options = {}) { if (!audioBlob || !(audioBlob instanceof Blob)) { console.error('AudioManager: Invalid speech audio blob provided'); return false; } try { // Create object URL from Blob const audioUrl = URL.createObjectURL(audioBlob); // Stop any current non-looping audio if (this.currentAudio) { this.currentAudio.pause(); this.currentAudio.currentTime = 0; } // Create new audio element const audio = new Audio(audioUrl); // Get speech volume from options or preferences const persistenceManager = this.getModule('persistence-manager'); let speechVolume = 1.0; if (persistenceManager) { // Get speech volume from preferences (0.0 to 1.0) speechVolume = persistenceManager.getPreference('tts', 'volume') || 1.0; } // Override with options if provided if (options.volume !== undefined) { speechVolume = options.volume; } // Apply master volume and speech volume audio.volume = this.masterVolume * speechVolume * this._ttsVolume; // Set up cleanup audio.onended = () => { URL.revokeObjectURL(audioUrl); if (this.currentAudio === audio) { this.currentAudio = null; } if (options.onComplete && typeof options.onComplete === 'function') { options.onComplete(); } // Dispatch event for speech completion window.dispatchEvent(new CustomEvent('tts:speak-completed')); }; // Handle errors audio.onerror = (error) => { console.error('AudioManager: Error playing speech audio:', error); URL.revokeObjectURL(audioUrl); if (this.currentAudio === audio) { this.currentAudio = null; } if (options.onError && typeof options.onError === 'function') { options.onError(error); } // Dispatch event for speech error window.dispatchEvent(new CustomEvent('tts:speak-error', { detail: { error: error } })); }; // Store as current audio this.currentAudio = audio; // Play the audio await audio.play(); return true; } catch (error) { console.error('AudioManager: Error setting up speech audio:', error); // Dispatch event for speech error window.dispatchEvent(new CustomEvent('tts:speak-error', { detail: { error: error } })); return false; } } } // Create the singleton instance const AudioManager = new AudioManagerModule(); // Export the module export { AudioManager };