diff --git a/src/components/AudioArea.vue b/src/components/AudioArea.vue index 351f20e..fd7fec4 100644 --- a/src/components/AudioArea.vue +++ b/src/components/AudioArea.vue @@ -27,19 +27,16 @@ export default defineComponent({ console.debug(`[AUDIOAREA] Initializing ${props.definition.src}...`); console.debug(props.definition); - // Silent 1ms audio as placeholder (data URI of a minimal silent MP3) - // This prevents browser from attempting to load any audio until we're ready - const SILENT_AUDIO = 'data:audio/mp3;base64,SUQzAwAAAAAAJlRJVDIAAAAHAAAAU2lsZW50/+MYxAAEaAIAAgAAAAkBngQAAABMQVJHAAAABQAA/+MYxA8EaAIAAgAAABAAAP/7kMQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/4xjEMgAAAAACAAAAAAAAAP/jGMRFAAAAAAAAAAAAAAAAAAAA'; - // Preload the audio file completely to avoid keeping connections open // Use the global audio loading queue to throttle concurrent loads const preloadAudio = (src: string) => { console.debug(`[AUDIOAREA] Queueing audio for preload: ${src}`); - - // Set placeholder silent audio to avoid errors without triggering real load - audioSrc.value = SILENT_AUDIO; - + + // Set audioSrc to empty initially + audioSrc.value = ""; + // Queue the audio for loading through our global service + // Our improved AudioLoader will cache and deduplicate requests queueAudioForLoading(src) .then((blobUrl) => { // Use blob URL to avoid keeping connections open diff --git a/src/services/AudioLoader.ts b/src/services/AudioLoader.ts index e9a9fd2..0e75fc8 100644 --- a/src/services/AudioLoader.ts +++ b/src/services/AudioLoader.ts @@ -13,6 +13,12 @@ const audioQueue: Array<{ onError: (error: Error) => void; }> = []; +// Cache of loaded audio files (src -> blobUrl) +const loadedAudioCache: Record = {}; + +// Keep track of pending loads to avoid duplicates +const pendingLoads: Set = new Set(); + /** * Queue an audio file for loading, respecting the global concurrent loading limit * Returns a promise that resolves with the blob URL when loading is complete @@ -22,6 +28,35 @@ export function queueAudioForLoading( onComplete?: (blobUrl: string) => void, onError?: (error: Error) => void ): Promise { + // Return cached result immediately if available + if (loadedAudioCache[src]) { + console.debug(`[AudioLoader] Using cached audio for ${src}`); + const blobUrl = loadedAudioCache[src]; + if (onComplete) setTimeout(() => onComplete(blobUrl), 0); + return Promise.resolve(blobUrl); + } + + // If this source is already being loaded, add to existing promises + if (pendingLoads.has(src)) { + console.debug(`[AudioLoader] Already loading ${src}, adding to pending requests`); + return new Promise((resolve, reject) => { + audioQueue.push({ + src, + onComplete: (blobUrl) => { + if (onComplete) onComplete(blobUrl); + resolve(blobUrl); + }, + onError: (error) => { + if (onError) onError(error); + reject(error); + } + }); + }); + } + + // Mark as pending + pendingLoads.add(src); + return new Promise((resolve, reject) => { // Add to queue audioQueue.push({ @@ -45,28 +80,56 @@ export function queueAudioForLoading( * Process the next items in the queue if we have capacity */ function processQueue() { - // Load more audio files if we have capacity and items in the queue + // Group queue items by src to avoid duplicate loads + const nextBatch: Record void; + onError: (error: Error) => void; + }>> = {}; + + // Find next items to process while respecting MAX_CONCURRENT_LOADS while (activeLoads < MAX_CONCURRENT_LOADS && audioQueue.length > 0) { const next = audioQueue.shift(); - if (next) { - loadAudio(next.src, next.onComplete, next.onError); + if (!next) continue; + + // If already cached, complete immediately without consuming a slot + if (loadedAudioCache[next.src]) { + next.onComplete(loadedAudioCache[next.src]); + continue; } + + // Group by src + if (!nextBatch[next.src]) { + nextBatch[next.src] = []; + // Each unique src counts as one active load + activeLoads++; + } + + nextBatch[next.src].push({ + onComplete: next.onComplete, + onError: next.onError + }); } + + // Start loading each unique audio file + Object.entries(nextBatch).forEach(([src, handlers]) => { + loadAudio(src, handlers); + }); } /** * Internal function to handle the actual audio loading using fetch API * This ensures the entire audio file is downloaded + * @param src The source URL to load + * @param handlers Array of handlers to call when loading completes or fails */ async function loadAudio( src: string, - onComplete: (blobUrl: string) => void, - onError: (error: Error) => void + handlers: Array<{ + onComplete: (blobUrl: string) => void; + onError: (error: Error) => void; + }> ) { - // Increment active loads counter - activeLoads++; - - console.debug(`[AudioLoader] Loading ${src}`); + console.debug(`[AudioLoader] Loading ${src} (${handlers.length} listeners)`); try { // Fetch the entire audio file @@ -82,16 +145,22 @@ async function loadAudio( // Create a blob URL to use as the audio source const blobUrl = URL.createObjectURL(blob); + // Store in cache + loadedAudioCache[src] = blobUrl; + console.debug(`[AudioLoader] Successfully loaded ${src}`); - // Call completion handler - onComplete(blobUrl); + // Call all completion handlers + handlers.forEach(handler => handler.onComplete(blobUrl)); } catch (error) { console.error(`[AudioLoader] Error loading audio: ${error}`); - // Call error handler - onError(error as Error); + // Call all error handlers + handlers.forEach(handler => handler.onError(error as Error)); } finally { + // Remove from pending loads + pendingLoads.delete(src); + // Decrement counter when audio is loaded (or failed) activeLoads--;