import { getSoundscapeByTitle } from '~/tracks.config' import { ensureAudio, useAudioStore } from '~/stores/audio' import { watch } from 'vue' /** * Media Provider Composable * Verwaltet AudioElemente und Web Audio Nodes für Soundscapes. */ export function useMediaProvider() { const audioElements = new Map() /** * Gibt ein HTMLAudioElement zurück für ein Soundscape-Titel * @param title z.B. "Forest" */ const getAudioElementForTitle = async (title: string): Promise => { const src = getSoundscapeByTitle(title) || 'Forest' const audioElement = await createPlayableAudioElement(src) return audioElement } /** * Erstellt ein AudioElement, wartet bis es abspielbereit ist (canplay), * oder gibt nach Timeout einen Fehler zurück. * * @param {string} src - Die Quelle der Audiodatei * @param {number} timeoutMs - Maximale Wartezeit in Millisekunden * @returns {Promise} */ async function createPlayableAudioElement(src: string, timeoutMs = 10000): Promise { return new Promise((resolve, reject) => { const audio = new Audio(src) const timeout = setTimeout(() => { audio.src = '' reject(new Error(`AudioElement did not become playable within ${timeoutMs}ms`)) }, timeoutMs) const cleanup = () => { clearTimeout(timeout) audio.removeEventListener('canplay', onCanPlay) audio.removeEventListener('error', onError) } const onCanPlay = () => { audioElements.set(src, audio) cleanup() resolve(audio) } const onError = () => { cleanup() reject(new Error(`Failed to load audio: ${src}`)) } audio.addEventListener('canplay', onCanPlay) audio.addEventListener('error', onError) audio.load() // wichtig! }) } /** * Erstellt einen MediaElementAudioSourceNode + verbindet ihn mit masterGainNoise. * Fängt Fehler ab und gibt `null` zurück bei Problemen. */ const getMediaElementSourceByTitle = async (title: string): Promise<{ element: HTMLAudioElement; source: MediaElementAudioSourceNode } | null> => { const audioStore = useAudioStore() const audioContext = audioStore.getContext() const masterGain = audioStore.masterGainNoise if (!audioContext || !masterGain) { console.warn('[MediaProvider] AudioContext oder masterGainNoise fehlt.') return null } try { const element = await createPlayableAudioElement(title) if (!element) return null const source = audioContext.createMediaElementSource(element) source.connect(masterGain) return { element, source } } catch (err) { console.error('[MediaProvider] Fehler beim Erstellen von MediaElementSource:', err) await ensureAudio const source = audioContext.createMediaElementSource(await createPlayableAudioElement(title)) source.connect(masterGain) return null } } /** * Reaktiver Binder für die Lautstärke an masterGainNoise.gain */ const bindVolumeToMasterGain = () => { const audioStore = useAudioStore() const masterGain = audioStore.masterGainNoise if (!masterGain) return watch( () => audioStore.noiseVolume, async (volume) => { try { if(audioStore.audioContext) { masterGain.gain.setTargetAtTime(volume, audioStore.audioContext.currentTime, 0.05) } } catch (err) { console.warn('[MediaProvider] Fehler beim Setzen der Lautstärke:', err) if(audioStore && audioStore.audioContext) { await ensureAudio masterGain.gain.setTargetAtTime(volume, audioStore.audioContext.currentTime, 0.05) } } }, { immediate: true } ) } bindVolumeToMasterGain() return { getAudioElementForTitle, getMediaElementSourceByTitle, createPlayableAudioElement } }