mindboost-rnbo-test-project/composables/useMediaProvider.ts

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
}
}