183 lines
4.8 KiB
TypeScript
183 lines
4.8 KiB
TypeScript
import { useAudioStore } from '~/stores/audio'
|
|
import { useUserStore } from '~/stores/user'
|
|
|
|
|
|
export const useAudioReplacer = () => {
|
|
|
|
const activeReplacements = new Map<string, Promise<void>>()
|
|
|
|
/**
|
|
* Wartet auf `canplay` eines AudioElements oder bricht mit Timeout ab.
|
|
*/
|
|
const waitForCanPlay = (audio: HTMLAudioElement, timeout = 10000) => {
|
|
return new Promise<boolean>((resolve) => {
|
|
const timer = setTimeout(() => resolve(false), timeout)
|
|
audio.oncanplay = () => {
|
|
clearTimeout(timer)
|
|
resolve(true)
|
|
}
|
|
audio.onerror = () => {
|
|
clearTimeout(timer)
|
|
resolve(false)
|
|
}
|
|
audio.load()
|
|
})
|
|
}
|
|
|
|
|
|
/**
|
|
* Replace an Audio Tag with an smooth fade in and fade out.
|
|
* @param title, newSrc, duration
|
|
*/
|
|
|
|
const replaceAudioWithFade = async ({
|
|
newSrc,
|
|
duration = 1.5
|
|
}: {
|
|
title: string
|
|
newSrc: string
|
|
duration?: number
|
|
}) => {
|
|
const { getNextSoundscape } = usePlayerControls()
|
|
const title = getNextSoundscape()
|
|
|
|
// Verhindere gleichzeitige Ersetzungen
|
|
if (activeReplacements.has(title)) {
|
|
|
|
await activeReplacements.get(title)
|
|
}
|
|
|
|
const promise = (async () => {
|
|
const audioStore = useAudioStore()
|
|
await audioStore.ensureAudioContextRunning()
|
|
const ctx = audioStore.getContext()
|
|
const masterGain = audioStore.masterGainMusic
|
|
|
|
const oldAudio = document.getElementById(title) as HTMLAudioElement
|
|
if (!oldAudio || !ctx || !masterGain) {
|
|
return
|
|
}
|
|
|
|
// Step 1: Fade out + stop
|
|
await fadeOutAndStop(oldAudio, duration)
|
|
|
|
// Step 2: Replace
|
|
const newAudio = oldAudio.cloneNode(true) as HTMLAudioElement
|
|
newAudio.src = newSrc
|
|
newAudio.volume = 0
|
|
newAudio.loop = true
|
|
newAudio.hidden = true
|
|
const sink = useUserStore().audioOutputDevice as MediaDeviceInfo
|
|
newAudio.setSinkId(sink.deviceId)
|
|
|
|
// Remove previous MediaElementSource if it exists
|
|
try {
|
|
const clone = ctx.createMediaElementSource(new Audio())
|
|
clone.disconnect()
|
|
} catch (err) {
|
|
console.warn('[AudioReplacer] MediaElementSource clone check failed', err)
|
|
}
|
|
|
|
// Replace in DOM
|
|
oldAudio.replaceWith(newAudio)
|
|
|
|
// Step 3: Connect to WebAudio
|
|
let gainNode: GainNode | null = null
|
|
try {
|
|
const source = ctx.createMediaElementSource(newAudio)
|
|
gainNode = ctx.createGain()
|
|
source.connect(gainNode).connect(masterGain)
|
|
} catch (err) {
|
|
console.warn('[AudioReplacer] Could not attach WebAudio. Using fallback.')
|
|
}
|
|
|
|
// Step 4: Wait for canplay
|
|
await waitForCanPlay(newAudio, 4000)
|
|
|
|
// Step 5: Play and fade in
|
|
await newAudio.play().catch(console.error)
|
|
fadeVolume(newAudio, 0, 1, duration * 1000)
|
|
})()
|
|
|
|
activeReplacements.set(title, promise)
|
|
|
|
try {
|
|
await promise
|
|
} finally {
|
|
activeReplacements.delete(title)
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Faded ein AudioElement über WebAudio oder Volume auf 0 aus.
|
|
*/
|
|
const fadeOutAndStop = (audio: HTMLAudioElement, duration: number) => {
|
|
return new Promise<void>((resolve) => {
|
|
fadeVolume(audio, audio.volume, 0, duration * 1000)
|
|
setTimeout(() => {
|
|
audio.pause()
|
|
audio.src = ''
|
|
resolve()
|
|
}, duration * 1000)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Faded ein AudioElement über WebAudio oder Volume auf 1 ein.
|
|
*/
|
|
const fadeInAudio = async (
|
|
audio: HTMLAudioElement,
|
|
duration = 1.5
|
|
): Promise<void> => {
|
|
const audioStore = useAudioStore()
|
|
await audioStore.ensureAudioContextRunning()
|
|
const ctx = audioStore.getContext()
|
|
const masterGain = audioStore.masterGainMusic as GainNode
|
|
|
|
try {
|
|
const gainNode = ctx.createGain()
|
|
const source = ctx.createMediaElementSource(audio)
|
|
source.connect(gainNode).connect(masterGain)
|
|
|
|
gainNode.gain.setValueAtTime(0, ctx.currentTime)
|
|
gainNode.gain.linearRampToValueAtTime(1, ctx.currentTime + duration)
|
|
|
|
audio.play()
|
|
await new Promise((res) => setTimeout(res, duration * 1000))
|
|
} catch (error) {
|
|
audio.play()
|
|
await fadeVolume(audio, 0, 1, duration)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Faded die Lautstärke eines AudioElements linear.
|
|
*/
|
|
const fadeVolume = (audio: HTMLAudioElement, from: number, to: number, duration: number) => {
|
|
const steps = 20
|
|
const stepTime = duration / steps
|
|
let currentStep = 0
|
|
|
|
const fadeInterval = setInterval(() => {
|
|
currentStep++
|
|
const progress = currentStep / steps
|
|
const newVolume = from + (to - from) * progress
|
|
audio.volume = Math.max(0, Math.min(1, newVolume))
|
|
|
|
if (currentStep >= steps) {
|
|
clearInterval(fadeInterval)
|
|
audio.volume = to
|
|
}
|
|
}, stepTime)
|
|
}
|
|
|
|
return {
|
|
fadeOutAndStop,
|
|
fadeInAudio,
|
|
waitForCanPlay,
|
|
fadeVolume,
|
|
replaceAudioWithFade
|
|
}
|
|
}
|