mindboost-rnbo-test-project/components/experiments/AudioTagWebAudio.vue

241 lines
7.0 KiB
Vue

<template>
<h3> AudioTagWebAudio </h3>
<button @click="fetchAudio">Fetch </button> <br> fetched:{{ readyState }}<br><br>
<button v-if="readyState" @click="startAudio"> Play </button>
<button @click="mute">Mute</button>
<button @click="unmute">Unmute</button>
<p v-if="audioNodes.length > 0">
<!-- eslint-disable-next-line vue/require-v-for-key -->
</p><div v-for="node in audioNodes">
duration: {{ node.buffer?.duration }}
volume: {{ safeVolume }}
length: {{ node.buffer?.length }}
channels: {{ node.buffer?.numberOfChannels }}
duration: {{ node.buffer?.duration }}
</div>
</template>
<script lang="ts">
import type { Logger } from 'pino'
import { defineComponent, ref, watch, computed } from 'vue'
import { createAudioSource } from '~/lib/AudioFunctions'
import { ensureAudio, useAudioStore } from '~/stores/audio'
export default defineComponent({
name: 'AudioTagWebAudio',
props: {
src: {
type: String,
required: true
},
volume: {
type: Number,
default: 0.0
},
play: {
type: Boolean,
default: false
},
masterGain: {
type: GainNode,
default: null,
require: true
}
},
emits: ['canplay'],
setup (props, { emit: $emit }) {
const logger = useNuxtApp().$logger as Logger
const ctx = useAudioStore().getContext()
const readyState = ref(false)
const fadingState = ref(false)
const audioNodes = ref([] as Array<AudioBufferSourceNode>)
let gainNode: GainNode | null = null
let audioElement: AudioBufferSourceNode | null = null
const safeVolume = computed((): number => {
const volumeVal = props.volume as number
if (volumeVal >= 0 && volumeVal <= 1.2589254117941673) { return volumeVal }
return Math.abs(volumeVal - 0) < Math.abs(volumeVal - 1) ? 0 : 1.2589254117941673
})
onMounted(() => {
fetchAudio()
})
onBeforeUnmount(() => {
if (audioElement instanceof AudioBufferSourceNode) {
try {
if (audioElement.hasStarted) {
audioElement.stop()
}
} catch (e) {
logger.warn('Audio element could not be stopped cleanly:', e)
}
try {
audioElement.disconnect()
} catch (e) {
logger.warn('Audio element could not be disconnected:', e)
}
audioElement = null // ✅ freigeben
}
if (gainNode instanceof GainNode) {
try {
gainNode.gain.cancelScheduledValues(ctx.currentTime)
gainNode.disconnect()
} catch (e) {
logger.warn('Gain node cleanup failed:', e)
}
gainNode = null // ✅ freigeben
}
})
const emitReady = () => {
$emit('canplay')
readyState.value = true
}
const mute = (value: any) => {
gainNode?.gain.setValueAtTime(0, ctx.currentTime)
}
const unmute = (value: any) => {
gainNode?.gain.setValueAtTime(1, ctx.currentTime)
}
const connectGainNode = (source:AudioBufferSourceNode) => {
gainNode = ctx.createGain()
gainNode.gain.setValueAtTime(0, ctx.currentTime)
source.connect(gainNode)
gainNode.connect(props.masterGain)
}
const fetchAudio = async () => {
audioElement = null
audioElement = await createAudioSource(ctx, props.src)
audioNodes.value.push(audioElement)
if (audioElement instanceof AudioBufferSourceNode) {
connectGainNode(audioElement)
emitReady()
}
}
const recreateSourceNode = () => {
if (!ctx || !audioElement?.buffer || !gainNode) {
logger.error('Cannot recreate source node: missing context, buffer, or gain node.')
return
}
// Erstelle neue AudioBufferSourceNode
const newSource = ctx.createBufferSource()
newSource.buffer = audioElement.buffer
newSource.playbackRate.value = audioElement.playbackRate.value || 1
// Optional: Übertrage weitere Parameter falls nötig (looping, detune, etc.)
newSource.loop = audioElement.loop ?? false
// Verbinde mit GainNode
newSource.connect(gainNode)
// Ersetze die alte Referenz
audioElement = newSource
// Reset hasStarted
newSource.hasStarted = false
}
const startAudio = async () => {
await ensureAudio()
if (props.play === false) {
return
}
if (gainNode instanceof GainNode && audioElement instanceof AudioBufferSourceNode) {
if ((audioElement as any).hasStarted) {
recreateSourceNode()
}
gainNode.gain.setValueAtTime(0, ctx.currentTime)
audioElement.playbackRate.value = 1
try {
(audioElement as any).hasStarted = true
audioElement.start()
} catch (error) {
(audioElement as any).hasStarted = false
audioElement.playbackRate.value = 1
}
gainNode.gain.linearRampToValueAtTime(safeVolume.value, ctx.currentTime + 5)
} else {
logger.error('Missing required audioNodes.')
}
}
const stopAudio = () => {
if (gainNode instanceof GainNode && audioElement instanceof AudioBufferSourceNode) {
try {
// Sanftes Fade-Out
const currentTime = ctx.currentTime
gainNode.gain.cancelScheduledValues(currentTime)
gainNode.gain.setValueAtTime(gainNode.gain.value, currentTime)
gainNode.gain.linearRampToValueAtTime(0, currentTime + 0.5) // 0.5 Sek. Fade-Out
// Stoppen nach dem Fade-Out
setTimeout(() => {
try {
if (audioElement instanceof AudioBufferSourceNode && audioElement.hasStarted) {
audioElement?.stop()
const audioElementObj = audioElement as any
}
} catch (error) {
logger.error('Error stopping audioElement:', { error })
}
}, 500)
} catch (error) {
logger.warn('Error during stopAudio:', error)
}
} else {
logger.error('Missing required audioNodes for stopping.')
}
}
watch(() => props.play, () => {
if (props.play && audioElement && readyState.value) {
try {
startAudio()
} catch (error) {
logger.warn('Error while start audio', error)
}
} else if (audioElement && !props.play) {
stopAudio()
}
})
watch(() => props.src, async () => {
if (props.src === '') {
logger.warn('Audio-Source is empty. Please check your props.')
return
}
await fetchAudio()
})
watch(() => props.volume, () => {
const bla = 0
if (!readyState.value) {
logger.warn('Audio is not yet ready for playing.')
return
}
if (props.play) {
if (gainNode instanceof GainNode) {
gainNode.gain.exponentialRampToValueAtTime(safeVolume.value, ctx.currentTime + 15)
}
}
})
return {
safeVolume,
emitReady,
fetchAudio,
readyState,
fadingState,
startAudio,
gainNode,
audioNodes,
mute,
unmute
}
}
})
</script>