Initial commit
This commit is contained in:
240
components/experiments/AudioTagWebAudio.vue
Normal file
240
components/experiments/AudioTagWebAudio.vue
Normal file
@@ -0,0 +1,240 @@
|
||||
<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>
|
Reference in New Issue
Block a user