Spaces:
Paused
Paused
File size: 6,423 Bytes
c408dda 3a7a84c c408dda 3a7a84c c408dda 3a7a84c c408dda 3a7a84c c408dda 3a7a84c c408dda 3a7a84c c408dda 3a7a84c c408dda 3a7a84c c408dda 3a7a84c c408dda 3a7a84c c408dda 3a7a84c c408dda 3a7a84c c408dda 3a7a84c c408dda 3a7a84c c408dda 3a7a84c c408dda 3a7a84c c408dda 3a7a84c c408dda 3a7a84c c408dda 3a7a84c c408dda 3a7a84c c408dda 3a7a84c c408dda 3a7a84c c408dda 3a7a84c c408dda 3a7a84c c408dda 3a7a84c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 | // 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(); |