Initial commit
This commit is contained in:
58
components/Player/HowlElement.vue
Normal file
58
components/Player/HowlElement.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<!-- // eslint-disable-next-line vue/multi-word-component-names -->
|
||||
<template>
|
||||
<div class="howl-player">
|
||||
<button @click="togglePlay">
|
||||
{{ isPlaying ? 'Pause' : 'Play' }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { Howl } from 'howler'
|
||||
import tracksConfig from '~/tracks.config'
|
||||
|
||||
// === PROPS ===
|
||||
const audioUrl = tracksConfig.debug_src
|
||||
|
||||
// === STATE ===
|
||||
const isPlaying = ref(false)
|
||||
let sound: Howl | null = null
|
||||
|
||||
// === LOGIK ===
|
||||
const togglePlay = () => {
|
||||
if (!sound) { return }
|
||||
|
||||
if (isPlaying.value) {
|
||||
sound.pause()
|
||||
isPlaying.value = false
|
||||
} else {
|
||||
sound.play()
|
||||
isPlaying.value = true
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
sound = new Howl({
|
||||
src: [audioUrl],
|
||||
html5: true,
|
||||
onend: () => {
|
||||
isPlaying.value = false
|
||||
},
|
||||
onplayerror: (id, err) => {
|
||||
useNuxtApp().$logger.warn('Playback error:', err)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
sound?.unload()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
button {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
</style>
|
66
components/Player/HowlWebAudioBridge.vue
Normal file
66
components/Player/HowlWebAudioBridge.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div class="howl-web-audio"> Howl as HTML5 with AudioNode - Node's ReadyState = {{ canPlay }} </div>
|
||||
<div v-if="canPlay" style="width: 2em; height: 2em; background-color:green;" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { Howl } from 'howler'
|
||||
import type { Logger } from 'pino'
|
||||
import { checkClipping, setupAudioSource } from '~/lib/AudioFunctions'
|
||||
|
||||
const logger = useNuxtApp().$logger as Logger
|
||||
|
||||
const props = defineProps<{
|
||||
src: string
|
||||
audioContext: AudioContext
|
||||
onReady?:(node: MediaElementAudioSourceNode | AudioBufferSourceNode | MediaStreamAudioSourceNode, howl: Howl | null) => void
|
||||
}>()
|
||||
|
||||
let howl: Howl | null = null
|
||||
let mediaElement: HTMLAudioElement | null = null
|
||||
const mediaStreamSource: MediaElementAudioSourceNode | AudioBufferSourceNode | MediaStreamAudioSourceNode | null = null
|
||||
const canPlay = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
// Initialisiere Howler im HTML5-Modus
|
||||
howl = new Howl({
|
||||
src: [props.src],
|
||||
html5: true,
|
||||
preload: true,
|
||||
onplay: () => {
|
||||
logger.info('howl is playing now')
|
||||
},
|
||||
onpause: () => { logger.info('howl is paused now') },
|
||||
onend: () => {
|
||||
logger.info('howl has ended now')
|
||||
canPlay.value = false
|
||||
},
|
||||
onload: async () => {
|
||||
// Zugriff auf das echte <audio> Element
|
||||
mediaElement = await (howl as any)._sounds[0]._node as HTMLAudioElement
|
||||
// mediaStreamSource = await setupAudioSource(mediaElement, props.audioContext)
|
||||
const mediaElementSource = props.audioContext.createMediaElementSource(mediaElement) // AudioNode spielt sofort
|
||||
logger.info('AudioNode sollte Ready sein', { mediaElementSource })
|
||||
if (mediaElementSource instanceof AudioNode) {
|
||||
// readyState = true
|
||||
props.onReady?.(mediaElementSource, howl)
|
||||
canPlay.value = true
|
||||
}
|
||||
logger.info('howl has loaded now')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
howl?.unload()
|
||||
canPlay.value = false
|
||||
// mediaStreamSource?.disconnect()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
button {
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
</style>
|
109
components/Player/Nodes.ts
Normal file
109
components/Player/Nodes.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { usePlayerStore } from '~/stores/player'
|
||||
import { createRNBODevice } from '~/lib/AudioFunctions'
|
||||
import importedMusicPatcher from 'assets/patch/music_patch.export.json'
|
||||
import importedNoisePatcher from 'assets/patch/noise_patch.export.json'
|
||||
import { ANC } from '~/stores/interfaces/ANC'
|
||||
import { HeadsetType } from '~/stores/interfaces/HeadsetType'
|
||||
import { useUserStore } from '~/stores/user'
|
||||
import { useMicStore } from '~/stores/microphone'
|
||||
|
||||
enum AttenuationFactor{
|
||||
OverEarANC = 0.0562,
|
||||
OverEar = 0.5623,
|
||||
InEar = 0.1778,
|
||||
InEarANC = 0.0316
|
||||
}
|
||||
|
||||
const logger = useNuxtApp().$logger
|
||||
|
||||
function getAttenuationFactor (anc:ANC, ht:HeadsetType): Number {
|
||||
if (anc === ANC.Yes && ht === HeadsetType.OverEar) { return AttenuationFactor.OverEarANC }
|
||||
if (anc === ANC.No && ht === HeadsetType.OverEar) { return AttenuationFactor.OverEar }
|
||||
if (anc === ANC.Yes && ht === HeadsetType.InEar) { return AttenuationFactor.InEarANC }
|
||||
if (anc === ANC.No && ht === HeadsetType.InEar) { return AttenuationFactor.InEar }
|
||||
return 0.5623
|
||||
}
|
||||
|
||||
export default async function setupNodes (noiseAudioSource: MediaElementAudioSourceNode, musicAudioSource: MediaElementAudioSourceNode): Promise<Array<GainNode> | null> {
|
||||
const playerStore = usePlayerStore()
|
||||
const microphone = await useMicStore().getMicrophoneInHowlerContext()
|
||||
const context = Howler.ctx
|
||||
|
||||
// Parameter validation
|
||||
// Parameter validation
|
||||
// Parameter validation
|
||||
if(!(noiseAudioSource instanceof AudioNode)) {
|
||||
useNuxtApp().$logger.warn("Parameter noiseAudioSource is not an AudioNode... skipping RNBO setup")
|
||||
return null
|
||||
}
|
||||
|
||||
if(!(musicAudioSource instanceof AudioNode)) {
|
||||
useNuxtApp().$logger.warn("Parameter musicAudioSource is not an AudioNode... skipping RNBO setup")
|
||||
return null
|
||||
}
|
||||
if(microphone.microphoneNode?.context !== noiseAudioSource.context && microphone.microphoneNode?.context !== musicAudioSource.context) {
|
||||
logger.error("Microphone and AudioSources are in different AudioContexts... skipping RNBO setup" )
|
||||
return null
|
||||
}
|
||||
|
||||
// Register the source nodes at the audio store
|
||||
// Register the source nodes at the audio store
|
||||
const noiseSource = playerStore.addNode('noiseAudioSource', noiseAudioSource)
|
||||
const musicSource = playerStore.addNode('musicAudioSource', musicAudioSource)
|
||||
|
||||
// Prepare gain nodes
|
||||
// Prepare gain nodes
|
||||
// Prepare gain nodes
|
||||
const musicGainNode = context.createGain()
|
||||
const noiseGainNode = context.createGain()
|
||||
musicGainNode.gain.value = 0
|
||||
noiseGainNode.gain.value = 0
|
||||
const musicGain = playerStore.addNode('musicGainNode', musicGainNode)
|
||||
const noiseGain = playerStore.addNode('noiseGainNode', noiseGainNode)
|
||||
|
||||
// create RNBO Devices
|
||||
// create RNBO Devices
|
||||
// create RNBO Devices
|
||||
const noiseDevice = await createRNBODevice(context, importedNoisePatcher)
|
||||
const musicDevice = await createRNBODevice(context, importedMusicPatcher)
|
||||
const noiseDeviceNode = playerStore.addNode('noiseDevice', noiseDevice.node)
|
||||
const musicDeviceNode = playerStore.addNode('musicDevice', musicDevice.node)
|
||||
|
||||
// create Channel-Splitter
|
||||
// create Channel-Splitter
|
||||
// create Channel-Splitter
|
||||
const musicInputSplit = playerStore.addNode('musicInputChannelSplitter', new ChannelSplitterNode(context, { numberOfOutputs: 2 }))
|
||||
const noiseInputSplit = playerStore.addNode('noiseInputChannelSplitter', new ChannelSplitterNode(context, { numberOfOutputs: 2 }))
|
||||
|
||||
// add microphone node
|
||||
// add microphone node
|
||||
// add microphone node
|
||||
const micNode = playerStore.addNode('microphone', microphone.microphoneNode)
|
||||
|
||||
|
||||
// MUSIC PATCH
|
||||
// MUSIC PATCH
|
||||
// MUSIC PATCH
|
||||
playerStore.connectNodes(micNode, musicDeviceNode, 0, 0)
|
||||
playerStore.connectNodes(musicSource, musicInputSplit, 0, 0)
|
||||
playerStore.connectNodes(musicInputSplit, musicDeviceNode, 0, 1)
|
||||
playerStore.connectNodes(musicInputSplit, musicDeviceNode, 1, 2)
|
||||
playerStore.connectNodes(musicDeviceNode, musicGain) // Ausgang zu Gain
|
||||
playerStore.connectNodes(musicGain, playerStore.addNode('contextDestination', context.destination)) // Output
|
||||
|
||||
// NOISE PATCH
|
||||
// NOISE PATCH
|
||||
// NOISE PATCH
|
||||
playerStore.connectNodes(micNode, noiseDeviceNode, 0, 0)
|
||||
playerStore.connectNodes(noiseSource, noiseInputSplit, 0, 0)
|
||||
playerStore.connectNodes(noiseInputSplit, noiseDeviceNode, 0, 1)
|
||||
playerStore.connectNodes(noiseInputSplit, noiseDeviceNode, 1, 2)
|
||||
playerStore.connectNodes(noiseDeviceNode, noiseGain)
|
||||
playerStore.connectNodes(noiseGain, playerStore.addNode('contextDestination', context.destination))
|
||||
|
||||
const attenuationFactor = noiseDevice.parametersById.get('attenuation')
|
||||
attenuationFactor.value = getAttenuationFactor('Yes' as ANC, 'OverEar' as HeadsetType)
|
||||
|
||||
// Return the gainNodes
|
||||
return [musicGainNode, noiseGainNode]
|
||||
}
|
221
components/Player/Player.js
Normal file
221
components/Player/Player.js
Normal file
@@ -0,0 +1,221 @@
|
||||
import { Howl } from 'howler'
|
||||
|
||||
function Player (playlist, location) {
|
||||
this.playlist = playlist.value
|
||||
this.location = location
|
||||
this.index = 0
|
||||
}
|
||||
Player.prototype = {
|
||||
setVolume (newVolume) {
|
||||
if (newVolume.isNaN()) {
|
||||
useNuxtApp().$logger.info('Passed volume to Player is NaN')
|
||||
return
|
||||
}
|
||||
const song = this.playlist[this.index]
|
||||
if (song.howl._html5) {
|
||||
song.howl.volume(this.volume)
|
||||
} else {
|
||||
useNuxtApp().$logger.info('WebAudio is used, control value over a GainNode')
|
||||
}
|
||||
},
|
||||
showMediaInformation (scenery) {
|
||||
if ('mediaSession' in navigator) {
|
||||
navigator.mediaSession.metadata = new MediaMetadata({
|
||||
title: scenery,
|
||||
artist: 'mindboost',
|
||||
album: 'mindboost Originale',
|
||||
artwork: [
|
||||
{ src: '/images/scenery/' + scenery + '.jpg', sizes: '96x96', type: 'image/jpeg' }
|
||||
]
|
||||
})
|
||||
}
|
||||
},
|
||||
createNewHowl (data) {
|
||||
// useNuxtApp().$logger.log(`/${location}/${data.file}.m4a`)
|
||||
// const sound = new Howl({ src: [src], html5: true, onend: () => { sound.unload(); }, });
|
||||
const myHowl = new Howl({
|
||||
src: [`/${this.location}/${data.file}.m4a`, `/${this.location}}/${data.file}.ogg`],
|
||||
html5: true,
|
||||
loop: true,
|
||||
autoplay: false,
|
||||
mute: false,
|
||||
onplay: () => {
|
||||
useNuxtApp().$logger.info('PLAYING SOUND')
|
||||
Howler.ctx.playbackRate = 1
|
||||
this.showMediaInformation(data.file.charAt(0).toUpperCase())
|
||||
// data.node = audioContext.createMediaElementSource(htmlElement)
|
||||
},
|
||||
onload: () => {
|
||||
useNuxtApp().$logger.log('Loading state: ' + data)
|
||||
},
|
||||
onend: () => {
|
||||
// Changed to arrow function
|
||||
Howler.ctx.playbackRate = 0
|
||||
data.sound = null
|
||||
data.node = null
|
||||
this.skipTo(this.index)
|
||||
},
|
||||
onpause: () => {
|
||||
// Stop the wave animation.
|
||||
Howler.ctx.playbackRate = 0
|
||||
data.sound = null
|
||||
data.node = null
|
||||
data.howl = null
|
||||
},
|
||||
onstop: () => {
|
||||
data.sound = null
|
||||
data.node = null
|
||||
}
|
||||
})
|
||||
return myHowl
|
||||
},
|
||||
initializeAudioNode (howl, index = 0) {
|
||||
if (howl._html5) {
|
||||
useNuxtApp().$logger.info('create audionode from HTML5 tag...')
|
||||
const mediaAudioNode = Howler.ctx.createMediaElementSource(howl._sounds[0]._node)
|
||||
this.playlist[index].node = mediaAudioNode
|
||||
return mediaAudioNode
|
||||
} else {
|
||||
useNuxtApp().$logger.info('create audionode from Web Audio API...')
|
||||
const audioContext = Howler.ctx
|
||||
this.playlist[index].node = howl._sounds[0]._node
|
||||
return howl._sounds[0]._node
|
||||
}
|
||||
},
|
||||
play () {
|
||||
useNuxtApp().$logger.info('play audionode on index ' + this.index)
|
||||
useNuxtApp().$logger.info('Anzahl der Howls = ' + this.playlist.filter(item => item.howl !== null).length)
|
||||
let sound = this.playlist[this.index].howl
|
||||
if (!sound) {
|
||||
this.skipTo(this.index)
|
||||
sound = this.playlist[this.index].howl
|
||||
sound.play()
|
||||
}
|
||||
const node = this.playlist[this.index].node
|
||||
if (sound._html5) {
|
||||
useNuxtApp().$logger.log('Audionode on html5 tag...', { sound })
|
||||
sound.play()
|
||||
// node.connect(Howler.ctx.destination)
|
||||
} else {
|
||||
useNuxtApp().$logger.log('Audionode on WebAudio node...', { sound })
|
||||
}
|
||||
// Get the Howl we want to manipulate.
|
||||
const title = this.playlist[this.index].title
|
||||
useNuxtApp().$logger.log('Audio Node', { sound })
|
||||
sound.play()
|
||||
useNuxtApp().$logger.log('Audio Node after play', { sound })
|
||||
this.addMediaNavigationHandling(title)
|
||||
},
|
||||
initializeHowl (index = 0) {
|
||||
const data = this.playlist[index]
|
||||
this.index = index
|
||||
let sound
|
||||
// If we already loaded this track, use the current one.
|
||||
if (data.howl) {
|
||||
sound = data.howl
|
||||
} else {
|
||||
sound = data.howl = this.createNewHowl(data)
|
||||
useNuxtApp().$logger.log({ sound })
|
||||
// data.node = this.initializeAudioNode(data.howl, index)
|
||||
useNuxtApp().$logger.log({ data })
|
||||
}
|
||||
return sound
|
||||
},
|
||||
pause () {
|
||||
// Get the Howl we want to manipulate.
|
||||
const testval = this.playlist[this.index]
|
||||
const testvalthis = this.playlist[this.index]
|
||||
useNuxtApp().$logger.log({ testval }, { testvalthis })
|
||||
if (this.playlist[this.index].howl) {
|
||||
const sound = this.playlist[this.index].howl
|
||||
// Pause the sound.
|
||||
sound.pause()
|
||||
}
|
||||
this.removeMediaNavigationHandling()
|
||||
},
|
||||
// Similarly, refactor other methods to use arrow functions where needed
|
||||
|
||||
/**
|
||||
* Skip to the next or previous track.
|
||||
* @param {String} direction 'next' or 'prev'.
|
||||
*/
|
||||
skip (direction) {
|
||||
let index = 0
|
||||
if (direction === 'prev') {
|
||||
index = this.index - 1
|
||||
if (index < 0) {
|
||||
index = this.playlist.length - 1
|
||||
}
|
||||
} else {
|
||||
index = this.index + 1
|
||||
if (index >= this.playlist.length) {
|
||||
index = 0
|
||||
}
|
||||
}
|
||||
this.skipTo(index)
|
||||
},
|
||||
skipTo (index) {
|
||||
// Stop the current track.
|
||||
if (this.playlist[this.index].howl) {
|
||||
this.playlist[this.index].howl.stop()
|
||||
if (this.playlist[this.index].howl._webAudio) { this.playlist[this.index].node.disconnect() }
|
||||
this.playlist[this.index].node = null
|
||||
this.playlist[this.index].howl = null
|
||||
}
|
||||
// Play the new track.
|
||||
this.initializeHowl(index)
|
||||
// sound.play()
|
||||
},
|
||||
setBarWidth (val) {
|
||||
// Update the display on the slider.
|
||||
const barWidth = (val * 90) / 100
|
||||
barFull.style.width = `${barWidth * 100}%`
|
||||
sliderBtn.style.left = `${window.innerWidth * barWidth + window.innerWidth * 0.05 - 25}px`
|
||||
},
|
||||
seek (per) {
|
||||
// Get the Howl we want to manipulate.
|
||||
const sound = this.playlist[this.index].howl
|
||||
|
||||
// Convert the percent into a seek position.
|
||||
if (sound.playing()) {
|
||||
sound.seek(sound.duration() * per)
|
||||
}
|
||||
},
|
||||
step () {
|
||||
// Use arrow function to preserve `this` context for `requestAnimationFrame`
|
||||
const stepUpdate = () => {
|
||||
// Get the Howl we want to manipulate.
|
||||
const sound = this.playlist[this.index].howl
|
||||
|
||||
// Determine our current seek position.
|
||||
const seek = sound.seek() || 0
|
||||
timer.innerHTML = this.formatTime(Math.round(seek))
|
||||
progress.style.width = `${(seek / sound.duration()) * 100 || 0}%`
|
||||
|
||||
// If the sound is still playing, continue stepping.
|
||||
if (sound.playing()) {
|
||||
requestAnimationFrame(stepUpdate)
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(stepUpdate)
|
||||
},
|
||||
togglePlaylist () {
|
||||
const display = (playlist.style.display === 'block') ? 'none' : 'block'
|
||||
|
||||
setTimeout(() => {
|
||||
playlist.style.display = display
|
||||
}, (display === 'block') ? 0 : 500)
|
||||
playlist.className = (display === 'block') ? 'fadein' : 'fadeout'
|
||||
},
|
||||
toggleVolume () {
|
||||
const display = (volumeDivRef.style.display === 'block') ? 'none' : 'block'
|
||||
|
||||
setTimeout(() => {
|
||||
volumeDivRef.style.display = display
|
||||
}, (display === 'block') ? 0 : 500)
|
||||
volumeDivRef.className = (display === 'block') ? 'fadein' : 'fadeout'
|
||||
}
|
||||
}
|
||||
|
||||
export default Player
|
Reference in New Issue
Block a user