Refactored modules and updated loader.
This commit is contained in:
@@ -0,0 +1,377 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
// 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<boolean>} - 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);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Play speech audio from a Blob
|
||||
* @param {Blob} audioBlob - The audio Blob to play
|
||||
* @param {Object} options - Playback options
|
||||
* @returns {Promise<boolean>} - 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;
|
||||
|
||||
// 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 };
|
||||
Reference in New Issue
Block a user