126 lines
3.9 KiB
TypeScript
126 lines
3.9 KiB
TypeScript
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<string, HTMLAudioElement>()
|
|
/**
|
|
* Gibt ein HTMLAudioElement zurück für ein Soundscape-Titel
|
|
* @param title z.B. "Forest"
|
|
*/
|
|
const getAudioElementForTitle = async (title: string): Promise<HTMLAudioElement | null> => {
|
|
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<HTMLAudioElement>}
|
|
*/
|
|
|
|
async function createPlayableAudioElement(src: string, timeoutMs = 10000): Promise<HTMLAudioElement> {
|
|
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
|
|
}
|
|
}
|