Spaces:
Paused
Paused
| // Define the webkitAudioContext for older browser compatibility (e.g., Safari) | |
| interface Window { | |
| webkitAudioContext?: typeof AudioContext; | |
| } | |
| /** | |
| * Manages all audio playback and recording stream routing for the application. | |
| * This is a singleton class, so you should import the `audioManager` instance. | |
| */ | |
| class AudioManager { | |
| private audioContext: AudioContext | null = null; | |
| private audioBuffers: Map<string, AudioBuffer> = new Map(); | |
| private isInitialized = false; | |
| // This node will be the destination for all sounds that need to be recorded. | |
| private mediaStreamDestination: MediaStreamAudioDestinationNode | null = null; | |
| /** | |
| * Initializes the AudioContext. Must be called before any other methods. | |
| * This is safe to call multiple times. | |
| */ | |
| public initialize() { | |
| if (this.isInitialized || this.audioContext) return; | |
| try { | |
| // Create a new AudioContext, using the webkit fallback if necessary. | |
| this.audioContext = new (window.AudioContext || (window as Window).webkitAudioContext)(); | |
| this.isInitialized = true; | |
| console.log("[AudioManager] AudioContext Initialized."); | |
| } catch (e) { | |
| console.error("[AudioManager] Web Audio API is not supported in this browser.", e); | |
| } | |
| } | |
| /** | |
| * Resumes the AudioContext if it's suspended. This must be called from | |
| * within a user interaction event (e.g., a click handler). | |
| */ | |
| public unlockAudio() { | |
| if (this.audioContext && this.audioContext.state === 'suspended') { | |
| this.audioContext.resume().then(() => { | |
| console.log("[AudioManager] AudioContext resumed successfully."); | |
| }).catch(e => console.error("[AudioManager] Error resuming AudioContext:", e)); | |
| } | |
| } | |
| /** | |
| * Fetches an audio file and decodes it into an AudioBuffer. | |
| * @param name - A unique key to identify the sound. | |
| * @param url - The URL of the audio file. | |
| */ | |
| public async loadSound(name: string, url: string): Promise<void> { | |
| if (!this.audioContext) { | |
| console.error("[AudioManager] AudioContext not initialized. Cannot load sound."); | |
| return Promise.reject("AudioContext not initialized"); | |
| } | |
| if (this.audioBuffers.has(name)) return Promise.resolve(); | |
| try { | |
| const response = await fetch(url, { mode: 'cors' }); | |
| const arrayBuffer = await response.arrayBuffer(); | |
| const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer); | |
| this.audioBuffers.set(name, audioBuffer); | |
| // console.log(`[AudioManager] Sound loaded: ${name}`); | |
| } catch (e) { | |
| console.error(`[AudioManager] Failed to load sound: ${name} from ${url}`, e); | |
| return Promise.reject(`Failed to load sound: ${name}`); | |
| } | |
| } | |
| /** | |
| * **[NEW METHOD]** | |
| * Retrieves a pre-loaded AudioBuffer by its name. | |
| * This is useful for getting metadata like the duration of a sound. | |
| * @param name - The key of the sound to retrieve. | |
| * @returns The AudioBuffer if found, otherwise null. | |
| */ | |
| public getSoundBuffer(name: string): AudioBuffer | null { | |
| return this.audioBuffers.get(name) || null; | |
| } | |
| /** | |
| * Plays a pre-loaded sound. | |
| * @param name - The key of the sound to play. | |
| * @param volume - The volume to play the sound at (0.0 to 1.0). | |
| */ | |
| public playSound(name: string, volume: number = 1) { | |
| if (!this.audioContext || !this.audioBuffers.has(name)) { | |
| // This can be noisy, so it's commented out, but useful for debugging. | |
| // console.warn(`[AudioManager] Sound not found or context not ready: ${name}`); | |
| return; | |
| } | |
| // Create a source node for the sound buffer. | |
| const source = this.audioContext.createBufferSource(); | |
| source.buffer = this.audioBuffers.get(name)!; | |
| // Create a GainNode to control the volume of this specific sound instance. | |
| const gainNode = this.audioContext.createGain(); | |
| gainNode.gain.value = volume; | |
| // The audio signal path: | |
| // Source -> GainNode -> ... | |
| source.connect(gainNode); | |
| // ... -> Main Output (Speakers) | |
| gainNode.connect(this.audioContext.destination); | |
| // ... -> Recording Stream (if active) | |
| if (this.mediaStreamDestination) { | |
| gainNode.connect(this.mediaStreamDestination); | |
| } | |
| source.start(0); | |
| } | |
| // --- RECORDING STREAM LIFECYCLE METHODS --- | |
| /** | |
| * **[IMPORTANT]** Creates the audio destination node for recording. | |
| * This must be called *before* you intend to start recording and *before* | |
| * any sounds that need to be recorded are played. | |
| */ | |
| public startAudioStreamCapture() { | |
| if (!this.audioContext) { | |
| console.error("[AudioManager] AudioContext not initialized. Cannot start stream capture."); | |
| return; | |
| } | |
| if (!this.mediaStreamDestination) { | |
| console.log("[AudioManager] Creating new MediaStreamAudioDestinationNode for recording."); | |
| this.mediaStreamDestination = this.audioContext.createMediaStreamDestination(); | |
| } | |
| } | |
| /** | |
| * Retrieves the audio stream for the MediaRecorder. | |
| * `startAudioStreamCapture` must have been called before this. | |
| * @returns A MediaStream containing the audio, or null if not ready. | |
| */ | |
| public getAudioStream(): MediaStream | null { | |
| if (!this.mediaStreamDestination) { | |
| console.error( | |
| "[AudioManager] Audio stream capture was not started. Call startAudioStreamCapture() first." | |
| ); | |
| return null; | |
| } | |
| return this.mediaStreamDestination.stream; | |
| } | |
| /** | |
| * Cleans up the recording stream destination. | |
| * Call this when the recording is finished. | |
| */ | |
| public stopAudioStreamCapture() { | |
| if (this.mediaStreamDestination) { | |
| // Nullifying the reference is enough for it to be garbage collected. | |
| this.mediaStreamDestination = null; | |
| console.log("[AudioManager] Cleared MediaStreamAudioDestinationNode."); | |
| } | |
| } | |
| } | |
| // Export a single instance to be used throughout the application. | |
| export const audioManager = new AudioManager(); |