/** * AudioManager Module * Manages loading and playback of non-TTS audio effects triggered by tags. */ import { BaseModule } from './base-module.js'; import { moduleRegistry } from './module-registry.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; } /** * 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 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); }); } } // Create the singleton instance const AudioManager = new AudioManagerModule(); // Register with the module registry moduleRegistry.register(AudioManager); // Export the module export { AudioManager }; // Keep a reference in window for loader system window.AudioManager = AudioManager;