diff --git a/src/components/AudioArea.vue b/src/components/AudioArea.vue index 2be1cad..1b3078f 100644 --- a/src/components/AudioArea.vue +++ b/src/components/AudioArea.vue @@ -5,6 +5,7 @@ - + diff --git a/src/services/AudioLoader.ts b/src/services/AudioLoader.ts new file mode 100644 index 0000000..0e75fc8 --- /dev/null +++ b/src/services/AudioLoader.ts @@ -0,0 +1,170 @@ +/** + * Global audio loading queue service to prevent hitting browser connection limits + */ + +// Configuration +const MAX_CONCURRENT_LOADS = 3; + +// State +let activeLoads = 0; +const audioQueue: Array<{ + src: string; + onComplete: (blobUrl: string) => void; + 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 + */ +export function queueAudioForLoading( + src: string, + 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({ + src, + onComplete: (blobUrl) => { + if (onComplete) onComplete(blobUrl); + resolve(blobUrl); + }, + onError: (error) => { + if (onError) onError(error); + reject(error); + } + }); + + // Try to process queue + processQueue(); + }); +} + +/** + * Process the next items in the queue if we have capacity + */ +function processQueue() { + // 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) 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, + handlers: Array<{ + onComplete: (blobUrl: string) => void; + onError: (error: Error) => void; + }> +) { + console.debug(`[AudioLoader] Loading ${src} (${handlers.length} listeners)`); + + try { + // Fetch the entire audio file + const response = await fetch(src); + + if (!response.ok) { + throw new Error(`Failed to load audio: ${response.statusText}`); + } + + // Convert to blob to ensure full download + const blob = await response.blob(); + + // 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 all completion handlers + handlers.forEach(handler => handler.onComplete(blobUrl)); + } catch (error) { + console.error(`[AudioLoader] Error loading audio: ${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--; + + // Process next audio in queue if available + processQueue(); + } +}