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

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