Initial commit
443
components/AcusticCheck.vue
Normal file
@@ -0,0 +1,443 @@
|
||||
<!-- eslint-disable no-console
|
||||
|
||||
THIS FILE IS NOT WORKING
|
||||
|
||||
-->
|
||||
<script>
|
||||
import { mapActions, mapState } from 'pinia'
|
||||
import { useAudioStore } from '@/stores/audio'
|
||||
export default {
|
||||
name: 'AdaptiveSoundscape',
|
||||
components: {},
|
||||
props: {},
|
||||
data () {
|
||||
return {
|
||||
title: 'Adaptive Soundscape'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(useAudioStore, ['microphone', 'acusticCheckResult']),
|
||||
audioStore () {
|
||||
return useAudioStore()
|
||||
}
|
||||
},
|
||||
watch: {},
|
||||
mounted () {
|
||||
this.StartButtonPressed()
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useAudioStore, ['initializeAudioContext', 'resetAudioContext']),
|
||||
data: () => ({
|
||||
// This object holds all state of the app
|
||||
|
||||
// meineReaktiveVariable: "Leon",
|
||||
AlteSeite: 0,
|
||||
NeueSeite: 0,
|
||||
pdf: 0,
|
||||
valid: true,
|
||||
Vorname: '',
|
||||
Nachname: '',
|
||||
nameRules: [
|
||||
v => !!v || 'Feld muss ausgefüllt sein'
|
||||
],
|
||||
Anrede: '',
|
||||
Mail: '',
|
||||
emailRules: [
|
||||
v => !!v || 'Feld muss ausgefüllt sein',
|
||||
v => /.+@.+\..+/.test(v) || 'Eingabe ungültig'
|
||||
],
|
||||
Firma: '',
|
||||
Raum: '',
|
||||
Zusatzinfos: '',
|
||||
|
||||
date: {},
|
||||
time: {},
|
||||
|
||||
Timeframe: 250,
|
||||
UpperPercentile: 0.9,
|
||||
LowerPercentile: 0.1,
|
||||
LimitA: 2.5,
|
||||
LimitB: 10.0,
|
||||
|
||||
audioCtx: {},
|
||||
source: {},
|
||||
audiodevices: {},
|
||||
|
||||
Gain: {},
|
||||
HPF: {},
|
||||
LPF: {},
|
||||
analyser: {},
|
||||
LevelAnalyser: {},
|
||||
LevelBuffer: {},
|
||||
|
||||
AnimationID: {},
|
||||
id: {},
|
||||
|
||||
bufferLength: {},
|
||||
dataArray: {},
|
||||
RecentRMS: 0,
|
||||
ByteToDecimal: 0,
|
||||
RecentLevelValue: 0,
|
||||
|
||||
ArrayCounter: 0,
|
||||
LevelValues: [],
|
||||
CummulatedLevelValues: 0,
|
||||
SortedArray: [],
|
||||
IndexMax: {},
|
||||
IndexMin: {},
|
||||
PercentileValue: {},
|
||||
|
||||
height: 0,
|
||||
value: 1,
|
||||
secondsDown: 25,
|
||||
|
||||
Result: {},
|
||||
ResultABC: {},
|
||||
|
||||
MicError: 0
|
||||
|
||||
}),
|
||||
|
||||
computed: {
|
||||
// Computed properties (state derived from data properties)
|
||||
},
|
||||
methods: {
|
||||
// Write your own functions that mutate data via `this.`
|
||||
|
||||
goto (refName) {
|
||||
const element = this.$refs[refName]
|
||||
const top = element.offsetTop
|
||||
|
||||
window.scrollTo(0, top)
|
||||
},
|
||||
|
||||
nextPage () {
|
||||
// this.seite = (this.seite + 1) % 3;
|
||||
this.AlteSeite = this.NeueSeite
|
||||
this.NeueSeite = (this.NeueSeite + 1)
|
||||
},
|
||||
firstPage () {
|
||||
this.AlteSeite = this.NeueSeite
|
||||
this.NeueSeite = 0
|
||||
},
|
||||
previousPage () {
|
||||
this.NeueSeite = this.AlteSeite
|
||||
this.AlteSeite = (this.AlteSeite - 1)
|
||||
this.pdf = 0
|
||||
},
|
||||
|
||||
pdfForm () {
|
||||
this.pdf = 1
|
||||
},
|
||||
|
||||
about () {
|
||||
this.AlteSeite = this.NeueSeite
|
||||
this.NeueSeite = 10
|
||||
},
|
||||
|
||||
hyperImprint () {
|
||||
// window.open("https://dsi.informationssicherheit.fraunhofer.de/de/impressum/www.akustik-check.de/full","_blank");
|
||||
this.AlteSeite = this.NeueSeite
|
||||
this.NeueSeite = 11
|
||||
},
|
||||
|
||||
hyperDataprotection () {
|
||||
// window.open("https://dsi.informationssicherheit.fraunhofer.de/de/dsi/www.akustik-check.de/full","_blank");
|
||||
this.AlteSeite = this.NeueSeite
|
||||
this.NeueSeite = 12
|
||||
},
|
||||
|
||||
hyperBueroinfos () {
|
||||
window.open('https://www.youtube.com/watch?v=yAY1Gr5tcpw', '_blank')
|
||||
},
|
||||
|
||||
hyperBI () {
|
||||
window.open('http://www.buero-initiative.de/', '_blank')
|
||||
},
|
||||
|
||||
ResetLevelValues () {
|
||||
this.ArrayCounter = 0
|
||||
this.LevelValues = []
|
||||
},
|
||||
|
||||
RMStoDBFS (InputArray) {
|
||||
for (let i = 0; i < InputArray.length; i++) {
|
||||
InputArray[i] = 20 * Math.log10(InputArray[i])
|
||||
}
|
||||
},
|
||||
|
||||
numSort (a, b) {
|
||||
return (a - b)
|
||||
},
|
||||
|
||||
Percentile (InputArray) {
|
||||
this.SortedArray = InputArray.sort(vm.numSort)
|
||||
vm.RMStoDBFS(this.SortedArray)
|
||||
// useNuxtApp().$logger.log('Sortiertes Array DBFS: ' + SortedArray);
|
||||
|
||||
this.IndexMax = Math.round(this.SortedArray.length * this.UpperPercentile)
|
||||
this.IndexMin = Math.round(this.SortedArray.length * this.LowerPercentile)
|
||||
this.PercentileValue = this.SortedArray[this.IndexMax] - this.SortedArray[this.IndexMin]
|
||||
this.PercentileValue = this.PercentileValue.toFixed(2) // nach zwei Dazimalstellen abbrechen
|
||||
// useNuxtApp().$logger.log('percentile Value = ' + this.PercentileValue)
|
||||
|
||||
if (this.PercentileValue < this.LimitA) {
|
||||
this.Result = 1
|
||||
this.ResultABC = 'A'
|
||||
this.acusticCheckResult = 1
|
||||
} else if (this.PercentileValue < this.LimitB) {
|
||||
this.Result = 2
|
||||
this.ResultABC = 'B'
|
||||
this.acusticCheckResult = 2
|
||||
} else {
|
||||
this.Result = 3
|
||||
this.ResultABC = 'C'
|
||||
this.acusticCheckResult = 3
|
||||
}
|
||||
// useNuxtApp().$logger.log('seite = ' + this.seite);
|
||||
|
||||
this.NeueSeite = 4
|
||||
vm.ResetLevelValues()
|
||||
},
|
||||
|
||||
stopAudioStream () {
|
||||
audiotracks.forEach((track) => {
|
||||
track.stop()
|
||||
})
|
||||
// useNuxtApp().$logger.log('Audiostream stopped.')
|
||||
},
|
||||
|
||||
stopmeasurement () {
|
||||
window.cancelAnimationFrame(this.AnimationID)
|
||||
clearInterval(this.id)
|
||||
// useNuxtApp().$logger.log('Measurement stopped.')
|
||||
|
||||
// stream.getTracks().forEach(function(track) {
|
||||
// rack.stop();
|
||||
// });
|
||||
|
||||
this.height = 0
|
||||
this.secondsDown = 25
|
||||
this.CummulatedLevelValues = 0
|
||||
|
||||
if (this.MicError === 0) {
|
||||
vm.stopAudioStream()
|
||||
}
|
||||
},
|
||||
|
||||
frame () {
|
||||
if (this.height === 100) {
|
||||
vm.stopmeasurement()
|
||||
vm.Percentile(this.LevelValues)
|
||||
} else {
|
||||
this.height++
|
||||
this.value = this.height + 2
|
||||
// useNuxtApp().$logger.log(this.height);
|
||||
if (this.height % 4 === 0) {
|
||||
this.secondsDown = 25 - (this.height / 4)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
startmeasurement () {
|
||||
// vm.visualize();
|
||||
vm.ResetLevelValues()
|
||||
|
||||
const today = new Date()
|
||||
this.date = today.getDate() + '.' + (today.getMonth() + 1) + '.' + today.getFullYear()
|
||||
this.time = today.getHours() + ':' + (today.getMinutes() < 10 ? '0' : '') + today.getMinutes()
|
||||
// requestAnimationFrame(vm.RecentLevel);
|
||||
this.height = 0
|
||||
requestAnimationFrame(vm.RecentLevel)
|
||||
this.id = setInterval(vm.frame, this.Timeframe)
|
||||
// vm.frame();
|
||||
},
|
||||
|
||||
CancleMeasurement () {
|
||||
vm.previousPage()
|
||||
this.MicError = 0
|
||||
},
|
||||
|
||||
// RMS Value
|
||||
RecentLevel () {
|
||||
self.LevelAnalyser.getByteTimeDomainData(self.LevelBuffer)
|
||||
|
||||
for (let i = 0; i < 512; i++) {
|
||||
this.ByteToDecimal = (self.LevelBuffer[i] - 128) / 128
|
||||
// useNuxtApp().$logger.log("LevelBuffer i = "+ self.LevelBuffer[i]);
|
||||
this.RecentRMS += this.ByteToDecimal * this.ByteToDecimal
|
||||
};
|
||||
this.RecentRMS /= 512
|
||||
this.RecentLevelValue = Math.sqrt(this.RecentRMS)
|
||||
|
||||
this.LevelValues[this.ArrayCounter] = this.RecentLevelValue
|
||||
|
||||
this.CummulatedLevelValues = this.CummulatedLevelValues + this.RecentLevelValue * 1000
|
||||
|
||||
if (this.ArrayCounter > 200 && this.CummulatedLevelValues < 50) {
|
||||
this.MicError = 1
|
||||
// clearInterval(this.id);
|
||||
vm.stopmeasurement()
|
||||
} else if (Object.isNaN(this.RecentLevelValue)) {
|
||||
this.MicError = 1
|
||||
// clearInterval(this.id);
|
||||
vm.stopmeasurement()
|
||||
} else if (this.RecentLevelValue > 0.7) {
|
||||
this.MicError = 2
|
||||
// clearInterval(this.id);
|
||||
vm.stopmeasurement()
|
||||
} else {
|
||||
++this.ArrayCounter
|
||||
this.AnimationID = requestAnimationFrame(vm.RecentLevel)
|
||||
}
|
||||
},
|
||||
|
||||
savePDF () {
|
||||
// eslint-disable-next-line new-cap
|
||||
const doc = new jspdf.jsPDF()
|
||||
let Perzentil = '0'
|
||||
if (this.PercentileValue > 3) {
|
||||
Perzentil = ((this.PercentileValue - 3).toFixed(2)).toString()
|
||||
};
|
||||
|
||||
const img1 = new Image()
|
||||
img1.src = 'img/ibp.925e79d4.png'
|
||||
doc.addImage(img1, 'png', 20, 10, 50, 15)
|
||||
|
||||
doc.setFontSize(24)
|
||||
doc.setTextColor('#1f82c0')
|
||||
doc.textWithLink('Büro-Akustik Check', 20, 45, { url: 'https://www.youtube.com/watch?v=yAY1Gr5tcpw' })
|
||||
|
||||
doc.setFontSize(16)
|
||||
doc.setTextColor('gray')
|
||||
doc.text('Auswertung der Messung am ' + this.date + ' um ' + this.time + ' Uhr:', 20, 60)
|
||||
|
||||
const img2 = new Image()
|
||||
img2.src = 'img/Result_' + this.ResultABC + '.png'
|
||||
doc.addImage(img2, 'png', 20, 70, 170, 80)
|
||||
|
||||
doc.setLineWidth(0.5)
|
||||
doc.setDrawColor('gray')
|
||||
doc.line(20, 170, 190, 170)
|
||||
|
||||
doc.setFontSize(11)
|
||||
doc.text('Gemessene akustische Qualität (kontaktieren Sie uns für weitere Informationen über diesen Wert):', 20, 180)
|
||||
doc.text('Messung durchgeführt von:', 20, 195)
|
||||
doc.text('Mailadresse:', 20, 210)
|
||||
doc.text('Firma:', 20, 225)
|
||||
doc.text('Raumbezeichnung:', 20, 240)
|
||||
doc.text('Zusatzinfos:', 20, 255)
|
||||
|
||||
doc.setTextColor('black')
|
||||
doc.text(Perzentil, 20, 185)
|
||||
doc.text(this.Anrede + this.Vorname + ' ' + this.Nachname, 20, 200)
|
||||
doc.text(this.Mail, 20, 215)
|
||||
doc.text(this.Firma, 20, 230)
|
||||
doc.text(this.Raum, 20, 245)
|
||||
|
||||
if (this.Zusatzinfos === '') {
|
||||
doc.text('-', 20, 260)
|
||||
} else { doc.text(this.Zusatzinfos, 20, 260) };
|
||||
|
||||
doc.text('Sie sind unzufrieden mit der Akustik an Ihrem Arbeitsplatz? Wir optimieren Ihr Büro menschzentriert.', 20, 275)
|
||||
doc.setTextColor('#1f82c0')
|
||||
doc.textWithLink('Kontaktieren Sie uns unter kognitive-ergonomie@ibp.fraunhofer.de', 20, 280, { url: 'mailto:kognitive-ergonomie@ibp.fraunhofer.de' })
|
||||
|
||||
// Speicherung lokal für User
|
||||
doc.save('Auswertung_Bueroumgebung.pdf')
|
||||
|
||||
// PDF an Server
|
||||
const blob = doc.output('blob')
|
||||
const formData = new FormData()
|
||||
formData.append('pdf', blob)
|
||||
$.ajax('/upload.php',
|
||||
{
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
success: function (data) { useNuxtApp().$logger.log(data) },
|
||||
error: function (data) { useNuxtApp().$logger.log(data) }
|
||||
})
|
||||
|
||||
//
|
||||
|
||||
// location.reload();
|
||||
},
|
||||
|
||||
audioSettings () {
|
||||
navigator.mediaDevices.enumerateDevices().then((devices) => {
|
||||
this.audiodevices = new Set()
|
||||
devices.forEach((device) => {
|
||||
// useNuxtApp().$logger.log(`${device.kind}: ${device.label} id = ${device.deviceId}`);
|
||||
if (device.kind === 'audioinput') {
|
||||
this.audiodevices.add(device)
|
||||
};
|
||||
})
|
||||
|
||||
// this.audiodevices.forEach((value) => {
|
||||
// useNuxtApp().$logger.log(value);
|
||||
// })
|
||||
// useNuxtApp().$logger.log('Verfügbare Audiodevices: ', this.audiodevices)
|
||||
})
|
||||
|
||||
this.settings = 1
|
||||
},
|
||||
|
||||
StartButtonPressed () {
|
||||
function getMedia () {
|
||||
let stream = null
|
||||
|
||||
try {
|
||||
stream = this.microphone.microphoneStream
|
||||
// useNuxtApp().$logger.log('Audiostream started.')
|
||||
audiotracks = stream.getTracks()
|
||||
|
||||
// this.audioCtx = new (window.AudioContext || window.webkitAudioContext)()
|
||||
this.audioCtx = this.getContext()
|
||||
// this.stream = navigator.mediaDevices.getUserMedia(constraints);
|
||||
this.Gain = this.audioCtx.createGain()
|
||||
this.Gain.gain.setValueAtTime(2.0, this.audioCtx.currentTime)
|
||||
|
||||
this.HPF = this.audioCtx.createBiquadFilter()
|
||||
this.HPF.type = 'highpass'
|
||||
this.HPF.frequency.value = 450
|
||||
|
||||
this.LPF = this.audioCtx.createBiquadFilter()
|
||||
this.LPF.type = 'lowpass'
|
||||
this.LPF.frequency.value = 10000
|
||||
|
||||
this.analyser = this.audioCtx.createAnalyser()
|
||||
this.analyser.fftSize = 256
|
||||
this.analyser.minDecibels = -90
|
||||
this.analyser.maxDecibels = -10
|
||||
this.analyser.smoothingTimeConstant = 0.9
|
||||
|
||||
this.LevelAnalyser = this.audioCtx.createAnalyser()
|
||||
this.LevelAnalyser.fftSize = 512
|
||||
this.LevelAnalyser.minDecibels = -100
|
||||
this.LevelAnalyser.maxDecibels = -1
|
||||
this.LevelAnalyser.smoothingTimeConstant = 0.95
|
||||
this.LevelBuffer = new Uint8Array(512)
|
||||
|
||||
this.source = this.audioCtx.createMediaStreamSource(stream)
|
||||
this.source.connect(this.Gain)
|
||||
this.Gain.connect(this.HPF)
|
||||
this.HPF.connect(this.LPF)
|
||||
this.LPF.connect(this.analyser)
|
||||
this.LPF.connect(this.LevelAnalyser)
|
||||
|
||||
this.FirstStart = false
|
||||
|
||||
setTimeout(vm.startmeasurement, 800) // short break to avoid zeros at beginning of measurement
|
||||
} catch (err) {
|
||||
// useNuxtApp().$logger.log('The following gUM error occured: ' + err)
|
||||
}
|
||||
}
|
||||
getMedia(constraints)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
~/stores/audio
|
133
components/AdaptiveModal.vue
Normal file
@@ -0,0 +1,133 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
defineProps({
|
||||
adaptiveSoundscape: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:adaptiveSoundscape'])
|
||||
|
||||
const handleChange = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
emit('update:adaptiveSoundscape', target.checked)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="adaptive-modal" class="modal fade " tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body">
|
||||
<div class=" bg-white " style="border-radius: 15px">
|
||||
<div class="row px-3">
|
||||
<div class="col-2">
|
||||
<span><i class="fa-solid fs-3 fa-arrow-left-long" style="cursor: pointer" data-bs-dismiss="modal" /></span>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<h4 class="text-center fw-bolder">
|
||||
{{ t('Adaptive soundscape') }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<div class="form-check form-switch float-end">
|
||||
<input
|
||||
id="flexSwitchCheckDefaultHomebar"
|
||||
class="form-check-input"
|
||||
:checked="adaptiveSoundscape"
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
@change="handleChange"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row px-2 pt-4">
|
||||
<div class="col-12 ps-3 fs-5" style="line-height: 25px">
|
||||
<p class="p-0 m-0">
|
||||
{{ t('The Mindboost sounscape responds to the acousitcs in your room.') }}
|
||||
</p>
|
||||
<p class="p-0 m-0" />
|
||||
<p class="">
|
||||
{{ t('Currently, your room has a:') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row align-items-center pt-4 ps-3">
|
||||
<div class="col-auto">
|
||||
<span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="52" height="54" viewBox="0 0 60 54" fill="none">
|
||||
<path d="M28.9879 0.600098C28.6223 0.600098 28.2613 0.675098 27.9004 0.787598C27.1738 1.01728 26.4801 1.44853 25.8754 2.0626L11.7754 16.1626H3.60039C1.59883 16.1626 -0.0371094 17.7985 -0.0371094 19.8001V34.2001C-0.0371094 36.2017 1.59883 37.8376 3.60039 37.8376H11.7754L25.8379 51.8251C27.1785 53.1704 28.8988 53.761 30.3004 53.1001C31.7113 52.4345 32.4004 50.8313 32.4004 48.9751V4.5751C32.4004 2.7751 31.4816 1.27978 30.0754 0.787598C29.7238 0.665723 29.3535 0.600098 28.9879 0.600098ZM29.0254 3.0001C29.1473 3.0001 29.241 3.00947 29.3254 3.0376C29.6676 3.15478 30.0004 3.45947 30.0004 4.5751V48.9751C30.0004 50.2548 29.5785 50.8267 29.2879 50.9626C28.9973 51.0985 28.4113 51.1126 27.4879 50.1751L12.3754 35.0626C12.352 35.0485 12.3238 35.0345 12.3004 35.0251C12.3895 35.1048 12.0004 34.3454 12.0004 33.9001V20.1001C12.0004 19.6548 12.4035 18.886 12.3004 18.9751C12.3285 18.9517 12.352 18.9282 12.3754 18.9001L27.5629 3.7501C28.1254 3.18291 28.6551 3.00479 29.0254 3.0001ZM45.9379 5.1751C45.4129 5.26416 45.0098 5.69072 44.9488 6.22041C44.8926 6.7501 45.1832 7.25635 45.6754 7.4626C52.8144 11.0626 57.6754 18.4454 57.6754 27.0001C57.6754 35.5548 52.8144 42.9376 45.6754 46.5376C45.2348 46.6876 44.9254 47.0813 44.8738 47.5407C44.8223 48.0048 45.0426 48.4548 45.441 48.6985C45.8348 48.9376 46.3363 48.9282 46.7254 48.6751C54.6473 44.6767 60.0754 36.4735 60.0754 27.0001C60.0754 17.5267 54.6473 9.32353 46.7254 5.3251C46.5191 5.20791 46.2848 5.15635 46.0504 5.1751C46.0129 5.1751 45.9754 5.1751 45.9379 5.1751ZM41.6629 11.5501C41.1285 11.6157 40.702 12.0329 40.6269 12.5673C40.5473 13.097 40.8332 13.6173 41.3254 13.8376C46.0082 16.3642 49.2004 21.2954 49.2004 27.0001C49.2004 32.686 46.0223 37.6313 41.3629 40.1626C40.7816 40.4813 40.5707 41.2126 40.8941 41.7938C41.2129 42.3751 41.9441 42.586 42.5254 42.2626C47.9348 39.3235 51.6004 33.5813 51.6004 27.0001C51.6004 20.4001 47.9254 14.6345 42.4879 11.7001C42.2723 11.5782 42.0238 11.5267 41.7754 11.5501C41.7379 11.5501 41.7004 11.5501 41.6629 11.5501ZM3.60039 18.6376H9.82539C9.6707 19.097 9.60039 19.5985 9.60039 20.1001V33.9001C9.60039 34.4017 9.67539 34.8985 9.82539 35.3626H3.60039C2.95352 35.3626 2.43789 34.847 2.43789 34.2001V19.8001C2.43789 19.1532 2.95352 18.6376 3.60039 18.6376ZM36.6379 19.0876C36.0566 19.1579 35.616 19.636 35.5879 20.222C35.5598 20.8032 35.9535 21.3235 36.5254 21.4501C38.9816 22.0923 40.8004 24.3329 40.8004 27.0001C40.8004 29.6673 38.9816 31.9079 36.5254 32.5501C35.8832 32.7142 35.4988 33.3704 35.6629 34.0126C35.827 34.6548 36.4832 35.0392 37.1254 34.8751C40.6176 33.961 43.2004 30.7642 43.2004 27.0001C43.2004 23.236 40.6176 20.0392 37.1254 19.1251C36.966 19.0782 36.802 19.0688 36.6379 19.0876Z" fill="#E9C046" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-11 col-md-10 ps-3 py-3">
|
||||
<h5>{{ t('Noisy Environment') }}</h5>
|
||||
<p class="mb-0">
|
||||
{{ t('The background noise at your workplace is disturbing. Your concentration is severely impaired. Mindboost protects you from the disturbing background noise.') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row align-items-center pt-3 ps-3 opacity-75">
|
||||
<div class="col-auto">
|
||||
<span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="52" height="54" viewBox="0 0 52 54" fill="none">
|
||||
<path d="M28.9879 0.600098C28.6223 0.600098 28.2613 0.675098 27.9004 0.787598C27.1738 1.01728 26.4801 1.44853 25.8754 2.0626L11.7754 16.1626H3.60039C1.59883 16.1626 -0.0371094 17.7985 -0.0371094 19.8001V34.2001C-0.0371094 36.2017 1.59883 37.8376 3.60039 37.8376H11.7754L25.8379 51.8251C27.1785 53.1704 28.8988 53.761 30.3004 53.1001C31.7113 52.4345 32.4004 50.8313 32.4004 48.9751V4.5751C32.4004 2.7751 31.4816 1.27978 30.0754 0.787598C29.7238 0.665723 29.3535 0.600098 28.9879 0.600098ZM29.0254 3.0001C29.1473 3.0001 29.241 3.00947 29.3254 3.0376C29.6676 3.15478 30.0004 3.45947 30.0004 4.5751V48.9751C30.0004 50.2548 29.5785 50.8267 29.2879 50.9626C28.9973 51.0985 28.4113 51.1126 27.4879 50.1751L12.3754 35.0626C12.352 35.0485 12.3238 35.0345 12.3004 35.0251C12.3895 35.1048 12.0004 34.3454 12.0004 33.9001V20.1001C12.0004 19.6548 12.4035 18.886 12.3004 18.9751C12.3285 18.9517 12.352 18.9282 12.3754 18.9001L27.5629 3.7501C28.1254 3.18291 28.6551 3.00479 29.0254 3.0001ZM41.6629 11.5501C41.1285 11.6157 40.702 12.0329 40.627 12.5673C40.5473 13.097 40.8332 13.6173 41.3254 13.8376C46.0082 16.3642 49.2004 21.2954 49.2004 27.0001C49.2004 32.686 46.0223 37.6313 41.3629 40.1626C40.7816 40.4813 40.5707 41.2126 40.8941 41.7938C41.2129 42.3751 41.9441 42.586 42.5254 42.2626C47.9348 39.3235 51.6004 33.5813 51.6004 27.0001C51.6004 20.4001 47.9254 14.6345 42.4879 11.7001C42.2723 11.5782 42.0238 11.5267 41.7754 11.5501C41.7379 11.5501 41.7004 11.5501 41.6629 11.5501ZM3.60039 18.6376H9.82539C9.6707 19.097 9.60039 19.5985 9.60039 20.1001V33.9001C9.60039 34.4017 9.67539 34.8985 9.82539 35.3626H3.60039C2.95352 35.3626 2.43789 34.847 2.43789 34.2001V19.8001C2.43789 19.1532 2.95352 18.6376 3.60039 18.6376ZM36.6379 19.0876C36.0566 19.1579 35.616 19.636 35.5879 20.222C35.5598 20.8032 35.9535 21.3235 36.5254 21.4501C38.9816 22.0923 40.8004 24.3329 40.8004 27.0001C40.8004 29.6673 38.9816 31.9079 36.5254 32.5501C35.8832 32.7142 35.4988 33.3704 35.6629 34.0126C35.827 34.6548 36.4832 35.0392 37.1254 34.8751C40.6176 33.961 43.2004 30.7642 43.2004 27.0001C43.2004 23.236 40.6176 20.0392 37.1254 19.1251C36.966 19.0782 36.802 19.0688 36.6379 19.0876Z" fill="#E9C046" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-11 col-md-10 ps-3 py-3">
|
||||
<h5>{{ t('Medium-noise Environment') }}</h5>
|
||||
<p class="mb-0">{{ t('The background noise at your workplace should be optimized. In the long term, it could disturb and have a negative impact on your health. Protect yourself with mindboost.') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row align-items-center pt-3 ps-3 opacity-50">
|
||||
<div class="col-auto">
|
||||
<span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="54" viewBox="0 0 44 54" fill="none">
|
||||
<path d="M28.9879 0.600098C28.6223 0.600098 28.2613 0.675098 27.9004 0.787598C27.1738 1.01728 26.4801 1.44853 25.8754 2.0626L11.7754 16.1626H3.60039C1.59883 16.1626 -0.0371094 17.7985 -0.0371094 19.8001V34.2001C-0.0371094 36.2017 1.59883 37.8376 3.60039 37.8376H11.7754L25.8379 51.8251C27.1785 53.1704 28.8988 53.761 30.3004 53.1001C31.7113 52.4345 32.4004 50.8313 32.4004 48.9751V4.5751C32.4004 2.7751 31.4816 1.27978 30.0754 0.787598C29.7238 0.665723 29.3535 0.600098 28.9879 0.600098ZM29.0254 3.0001C29.1473 3.0001 29.241 3.00947 29.3254 3.0376C29.6676 3.15478 30.0004 3.45947 30.0004 4.5751V48.9751C30.0004 50.2548 29.5785 50.8267 29.2879 50.9626C28.9973 51.0985 28.4113 51.1126 27.4879 50.1751L12.3754 35.0626C12.352 35.0485 12.3238 35.0345 12.3004 35.0251C12.3895 35.1048 12.0004 34.3454 12.0004 33.9001V20.1001C12.0004 19.6548 12.4035 18.886 12.3004 18.9751C12.3285 18.9517 12.352 18.9282 12.3754 18.9001L27.5629 3.7501C28.1254 3.18291 28.6551 3.00479 29.0254 3.0001ZM3.60039 18.6376H9.82539C9.6707 19.097 9.60039 19.5985 9.60039 20.1001V33.9001C9.60039 34.4017 9.67539 34.8985 9.82539 35.3626H3.60039C2.95352 35.3626 2.43789 34.847 2.43789 34.2001V19.8001C2.43789 19.1532 2.95352 18.6376 3.60039 18.6376ZM36.6379 19.0876C36.0566 19.1579 35.616 19.636 35.5879 20.222C35.5598 20.8032 35.9535 21.3235 36.5254 21.4501C38.9816 22.0923 40.8004 24.3329 40.8004 27.0001C40.8004 29.6673 38.9816 31.9079 36.5254 32.5501C35.8832 32.7142 35.4988 33.3704 35.6629 34.0126C35.827 34.6548 36.4832 35.0392 37.1254 34.8751C40.6176 33.961 43.2004 30.7642 43.2004 27.0001C43.2004 23.236 40.6176 20.0392 37.1254 19.1251C36.966 19.0782 36.802 19.0688 36.6379 19.0876Z" fill="#E9C046" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-11 col-md-10 ps-3 py-3">
|
||||
<h5>{{ t('Good Environment') }}</h5>
|
||||
<p class="mb-0">
|
||||
{{ t('The background noise at your workplace provides a long’term healthy basis for concentrated work. With Mindboost you make sure that even sudden disturbances do not distract you.') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.modal-body {
|
||||
padding: 2em 1.5em;
|
||||
}
|
||||
|
||||
.form-check-input {
|
||||
background-color: #f0f0f0 !important;
|
||||
|
||||
&:checked{
|
||||
background-color: #e9c046 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.adaptive .slider {
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
.adaptive {
|
||||
padding-bottom: 5em !important;
|
||||
}
|
||||
}
|
||||
</style>
|
36
components/AudioFileSelector.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div class="audio-file-selector">
|
||||
<select v-model="selectedFile" @change="onFileSelect">
|
||||
<option value="">Select an audio file</option>
|
||||
<option v-for="(src, title) in audioFiles" :key="title" :value="src">
|
||||
{{ title }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'AudioFileSelector',
|
||||
emits: ['file-selected'],
|
||||
setup (_, { emit }) {
|
||||
const config = useRuntimeConfig()
|
||||
const audioFiles = ref(config.public.tracks)
|
||||
|
||||
const selectedFile = ref('')
|
||||
|
||||
const onFileSelect = () => {
|
||||
const fullPath = window.location.origin + selectedFile.value
|
||||
emit('file-selected', fullPath)
|
||||
}
|
||||
|
||||
return {
|
||||
audioFiles,
|
||||
selectedFile,
|
||||
onFileSelect
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
113
components/AudioReactiveBar.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<div class="progress" style="height: 10px; width: 100%;">
|
||||
<div
|
||||
class="progress-bar bar"
|
||||
role="progressbar"
|
||||
aria-label="Basic example"
|
||||
:style="{width:bar_width+'%'}"
|
||||
style="background-color: #e9c046;transition: 0.1s"
|
||||
aria-valuenow="75"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="false">
|
||||
<h3>Debug Test</h3>
|
||||
<h4>Current Microphone: {{ microphoneLabel }}</h4>
|
||||
<h4>Current Steam ID: {{ deviceId }}</h4>
|
||||
<button @click="updateMicrophone">Change Microphone</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'pinia'
|
||||
import { useMicStore } from '@/stores/microphone'
|
||||
export default {
|
||||
components: {
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
t: useI18n,
|
||||
// meter
|
||||
meter: null,
|
||||
bar_width: 0,
|
||||
analyser: null,
|
||||
dataArray: null,
|
||||
bar_val: 25,
|
||||
microphoneLabel: null,
|
||||
microAnalyse: null,
|
||||
deviceId: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(useMicStore, 'microphone')
|
||||
},
|
||||
beforeUnmount () {
|
||||
if (this.audioContext) { this.audioContext.close() }
|
||||
this.audioContext = null
|
||||
useMicStore().detachMicrophone()
|
||||
},
|
||||
mounted () {
|
||||
// useNuxtApp().$logger.log('mounted ', this.microphone)
|
||||
useMicStore().getMicrophone().then((mic) => {
|
||||
// useNuxtApp().$logger.log('Microphone ready')
|
||||
const audioCtx = mic.microphoneNode.context
|
||||
useNuxtApp().$logger.log({ mic })
|
||||
this.microphoneLabel = mic.microphoneLabel
|
||||
this.deviceId = mic.deviceId || 'unknown'
|
||||
this.analyser = audioCtx.createAnalyser()
|
||||
mic.microphoneNode.connect(this.analyser)
|
||||
this.analyser.fftSize = 2048
|
||||
const bufferLength = this.analyser.frequencyBinCount
|
||||
this.dataArray = new Uint8Array(bufferLength)
|
||||
this.updateMeter()
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
updateMeter () {
|
||||
requestAnimationFrame(this.updateMeter)
|
||||
this.analyser.getByteFrequencyData(this.dataArray)
|
||||
const rms = this.getRMS(this.dataArray)
|
||||
let level = 90 * Math.log10(rms / 128)
|
||||
level = Math.max(0, Math.min(100, level + 100))
|
||||
// bar.style.width = level + '%'
|
||||
this.bar_width = level
|
||||
},
|
||||
getRMS (dataArray) {
|
||||
let rms = 0
|
||||
for (let i = 0; i < dataArray.length; i++) {
|
||||
rms += dataArray[i] * dataArray[i]
|
||||
}
|
||||
rms /= dataArray.length
|
||||
rms = Math.sqrt(rms)
|
||||
return rms
|
||||
},
|
||||
async updateMicrophone () {
|
||||
if (!(this.deviceId instanceof String)) {
|
||||
let devices = useMicStore().availableDevices
|
||||
devices = devices.filter(x => x.label !== this.microphoneLabel)
|
||||
const newDevice = devices.pop()
|
||||
this.microphoneLabel = newDevice.label
|
||||
this.deviceId = newDevice.deviceId
|
||||
await useMicStore().switchMicrophone(this.deviceId)
|
||||
}
|
||||
try {
|
||||
const mic = useMicStore().microphone
|
||||
if (mic && mic.microphoneNode) {
|
||||
const audioCtx = mic.microphoneNode.context
|
||||
this.analyser = audioCtx.createAnalyser()
|
||||
mic.microphoneNode.connect(this.analyser)
|
||||
this.analyser.fftSize = 2048
|
||||
const bufferLength = this.analyser.frequencyBinCount
|
||||
this.dataArray = new Uint8Array(bufferLength)
|
||||
this.updateMeter()
|
||||
}
|
||||
} catch (error) {
|
||||
useNuxtApp().$logger.error('Error updating microphone in AudioReactiveBar:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
</style>
|
419
components/BootomBar.vue
Normal file
687
components/NavigationBar.vue
Normal file
@@ -0,0 +1,687 @@
|
||||
<template>
|
||||
<div class="nav">
|
||||
<ul class="nav__list">
|
||||
<li id="step-1" class="nav__item">
|
||||
<a
|
||||
v-if="!isPlaying()"
|
||||
id="foucused-icon"
|
||||
:class="{'is-active': true}"
|
||||
class="nav__item-link"
|
||||
@click.prevent="playSound"
|
||||
>
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="m6.192 3.67 13.568 7.633a.8.8 0 0 1 0 1.394L6.192 20.33A.8.8 0 0 1 5 19.632V4.368a.8.8 0 0 1 1.192-.698Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a v-else id="foucused-icon" :class="{'is-active': false}" class="nav__item-link" @click.prevent="playSound">
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#a)">
|
||||
<path
|
||||
d="M17.083 19.917a2.326 2.326 0 0 1-1.706-.71 2.332 2.332 0 0 1-.71-1.707V5.417c0-.665.236-1.234.71-1.706A2.333 2.333 0 0 1 17.083 3c.664 0 1.233.236 1.708.71.474.475.71 1.044.709 1.707V17.5a2.33 2.33 0 0 1-.71 1.707 2.322 2.322 0 0 1-1.707.71Zm-9.666 0a2.326 2.326 0 0 1-1.707-.71A2.332 2.332 0 0 1 5 17.5V5.417c0-.665.237-1.234.71-1.706A2.333 2.333 0 0 1 7.417 3c.663 0 1.233.236 1.707.71.475.475.71 1.044.71 1.707V17.5a2.33 2.33 0 0 1-.71 1.707 2.322 2.322 0 0 1-1.707.71Z"
|
||||
fill="#e9c046"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="a">
|
||||
<path fill="#e9c046" d="M0 0h24v24H0z" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
id="step-2"
|
||||
ref="soundscape"
|
||||
class="nav__item nav__item--toggle"
|
||||
:class="{ 'nav__item--active': userStore.soundMode === 'soundscape' }"
|
||||
@click="setSoundMode('soundscape')"
|
||||
>
|
||||
<a class="nav__item-link" aria-current="page" href="#" @click.prevent="toggleDropdown('soundscape')">
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M8.286 18.875c0 .564-.226 1.104-.628 1.503a2.152 2.152 0 0 1-3.03 0 2.116 2.116 0 0 1 0-3.006 2.152 2.152 0 0 1 3.03 0c.402.399.628.94.628 1.503Zm0 0V6.479L19 4v12.396M8.286 10.73 19 8.25m0 8.5c0 .564-.226 1.104-.628 1.503a2.152 2.152 0 0 1-3.03 0 2.116 2.116 0 0 1 0-3.006 2.152 2.152 0 0 1 3.03 0c.402.399.628.94.628 1.503Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<ul
|
||||
class="nav__dropdown nav__dropdown--no-padding"
|
||||
:class="{ open: activeDropdown === 'soundscape' }"
|
||||
@click.stop
|
||||
>
|
||||
<li>
|
||||
<nuxt-link
|
||||
id="lagoonLinkHomebar"
|
||||
:class="{'is-select':form.soundscape=='Lagoon'}"
|
||||
class="nav__dropdown-listItem py-2 fw-bold fs-6 "
|
||||
@click="saveSetting('Lagoon', 'soundscape')"
|
||||
>
|
||||
{{ t('Lagoon') }}
|
||||
<span class="float-end">
|
||||
<WaveSVG />
|
||||
</span>
|
||||
</nuxt-link>
|
||||
</li>
|
||||
<li>
|
||||
<nuxt-link
|
||||
id="tropicsLinkHomebar"
|
||||
:class="{'is-select':form.soundscape=='Tropics'}"
|
||||
class="nav__dropdown-listItem fw-bold fs-6 py-2"
|
||||
@click="saveSetting('Tropics', 'soundscape')"
|
||||
>
|
||||
{{ t('Tropics') }}
|
||||
<span class="float-end">
|
||||
<TropicsSVG />
|
||||
</span>
|
||||
</nuxt-link>
|
||||
</li>
|
||||
<li>
|
||||
<nuxt-link
|
||||
id="forestLinkHomebar"
|
||||
:class="{'is-select':form.soundscape=='Forest'}"
|
||||
class="nav__dropdown-listItem fw-bold fs-6 py-2"
|
||||
@click="saveSetting('Forest', 'soundscape')"
|
||||
>
|
||||
{{ t('Forest') }}
|
||||
<span class="float-end">
|
||||
<ForestSVG />
|
||||
</span>
|
||||
</nuxt-link>
|
||||
</li>
|
||||
<li>
|
||||
<nuxt-link
|
||||
id="meadowLinkHomebar"
|
||||
:class="{'is-select':form.soundscape=='Meadow'}"
|
||||
class="nav__dropdown-listItem fw-bold fs-6 py-2"
|
||||
@click="saveSetting('Meadow', 'soundscape')"
|
||||
>
|
||||
{{ t('Meadow') }}
|
||||
<span class="float-end">
|
||||
<MeadowSVG />
|
||||
</span>
|
||||
</nuxt-link>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li
|
||||
id="step-3"
|
||||
ref="music"
|
||||
class="nav__item nav__item--toggle"
|
||||
:class="{ 'nav__item--active': userStore.soundMode === 'music' }"
|
||||
@click="setSoundMode('music')"
|
||||
>
|
||||
<a class="nav__item-link" aria-current="page" href="#" @click.prevent="toggleDropdown('music')">
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20.293 2 3 21.707" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
||||
<path
|
||||
d="M8.286 18.875c0 .564-.226 1.104-.628 1.503a2.152 2.152 0 0 1-3.03 0 2.116 2.116 0 0 1 0-3.006 2.152 2.152 0 0 1 3.03 0c.402.399.628.94.628 1.503Zm0 0V6.479L19 4v12.396M8.286 10.73 19 8.25m0 8.5c0 .564-.226 1.104-.628 1.503a2.152 2.152 0 0 1-3.03 0 2.116 2.116 0 0 1 0-3.006 2.152 2.152 0 0 1 3.03 0c.402.399.628.94.628 1.503Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<ul class="nav__dropdown text-muted-dark" :class="{ open: activeDropdown === 'music' }" @click.stop>
|
||||
<p class="fw-bold pt-1">{{ t('musicmode_activated') }}</p>
|
||||
<p>{{ t('musicmode_text') }}</p>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li id="step-4" ref="settings" class="nav__item" @click.stop="handleClickOutside">
|
||||
<a class="nav__item-link" aria-current="page" href="#" @click.stop="toggleDropdown('settings')">
|
||||
<svg width="18" height="18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M2.3 8.177V.05h2.15V8.205l.025.015a2.2 2.2 0 1 1-2.2 0l.025-.015v-.028Zm7.775-5.625v.028l.025.015a2.2 2.2 0 1 1-2.2 0l.025-.015V.05h2.15v2.502ZM15.7 8.177v.028l.025.015a2.2 2.2 0 1 1-2.2 0l.025-.015V.05h2.15v8.127ZM4.45 13.55v4.4H2.3v-4.4h2.15Zm5.625-5.625V17.95h-2.15V7.925h2.15Zm3.475 5.625h2.15v4.4h-2.15v-4.4Z"
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
stroke-width=".1"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<ul class="nav__dropdown pt-4" :class="{ open: activeDropdown === 'settings' }" @click.stop>
|
||||
<li v-for="soundscape in soundscapes" :key="soundscape.id">
|
||||
<MusicGainSlider v-if="currentIndex === soundscape.id && userStore.soundMode !== 'music'" />
|
||||
</li>
|
||||
<li>
|
||||
<NoiseGainSlider />
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li
|
||||
v-if="userStore.isTimerActivated"
|
||||
id="step-5"
|
||||
ref="pomodoro"
|
||||
class="nav__item nav__item--timer-bg"
|
||||
:style="`--progress-width: ${progressBarWidth}`"
|
||||
:data-phase="timer.getCurrentSession"
|
||||
:data-color="progressColor"
|
||||
@click.stop="handleClickOutside"
|
||||
>
|
||||
<a
|
||||
class="nav__item-link nav__item-link--timer"
|
||||
aria-current="page"
|
||||
:style="`--progress-color: ${progressColor}`"
|
||||
href="#"
|
||||
@click.stop="toggleDropdown('pomodoro')"
|
||||
>
|
||||
<svg width="27" height="27" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M15.11 6.301a7.78 7.78 0 0 1-1.111 15.478A7.777 7.777 0 0 1 8.734 8.275L7.162 6.703a10 10 0 1 0 5.726-2.64v6.7h2.222V6.3Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M9.229 10.342a1.11 1.11 0 0 0 0 1.571l3.143 3.144a1.111 1.111 0 0 0 1.571-1.572L10.8 10.343a1.111 1.111 0 0 0-1.57 0Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<ul class="nav__dropdown" :class="{ open: activeDropdown === 'pomodoro' }" @click.stop>
|
||||
<PomodoroTimer />
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { mapActions } from 'pinia'
|
||||
|
||||
import { driver } from 'driver.js'
|
||||
import MusicGainSlider from '../components/sliders/MusicGainSlider'
|
||||
import NoiseGainSlider from '../components/sliders/NoiseGainSlider'
|
||||
import AdaptiveNoiseGain from '../components/experiments/homepages/AdaptiveNoiseGain'
|
||||
import 'driver.js/dist/driver.css'
|
||||
|
||||
import WaveSVG from './svg/homebar/WaveSVG.vue'
|
||||
import TropicsSVG from './svg/homebar/TropicsSVG.vue'
|
||||
import ForestSVG from './svg/homebar/ForestSVG.vue'
|
||||
import MeadowSVG from './svg/homebar/MeadowSVG.vue'
|
||||
|
||||
import PomodoroTimer from '~/components/timer/PomodoroTimer.vue'
|
||||
|
||||
import { useTimerStore } from '~/stores/timer'
|
||||
import { useAudioStore } from '~/stores/audio'
|
||||
|
||||
import { calculateNormalizedVolume } from '~/lib/AudioFunctions'
|
||||
import { useUserStore } from '~/stores/user'
|
||||
|
||||
export default {
|
||||
name: 'NavigationBar',
|
||||
components: {
|
||||
WaveSVG,
|
||||
TropicsSVG,
|
||||
ForestSVG,
|
||||
MeadowSVG,
|
||||
NoiseGainSlider,
|
||||
MusicGainSlider,
|
||||
PomodoroTimer
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: 'NavigationBar'
|
||||
},
|
||||
currentIndex: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
currentSoundscape: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
adaptive: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['update:soundscape', 'update:currentIndex'],
|
||||
setup () {
|
||||
const { t } = useI18n()
|
||||
const logger = useNuxtApp.$logger
|
||||
const localePath = useLocalePath()
|
||||
const timer = useTimerStore()
|
||||
const audioStore = useAudioStore()
|
||||
const userStore = useUserStore()
|
||||
const { $axios } = useNuxtApp()
|
||||
|
||||
const activeDropdown = ref(null)
|
||||
|
||||
const progressBarWidth = computed(() => {
|
||||
const totalTime = timer.getSessionTime * 60
|
||||
return totalTime > 0 ? `${(timer.getTimeRemaining * 100) / totalTime}%` : '0%'
|
||||
})
|
||||
|
||||
const progressColor = computed(() => {
|
||||
return timer.getCurrentColor
|
||||
})
|
||||
|
||||
const handleValueChange = (value) => {
|
||||
// useNuxtApp().$logger.log('handle new control value: ' + value)
|
||||
if (!isNaN(value)) {
|
||||
const normaizedVolume = calculateNormalizedVolume(value)
|
||||
// useNuxtApp().$logger.log('normalized volume: ' + normaizedVolume)
|
||||
} else {
|
||||
logger.log('value is not NaN', { value })
|
||||
}
|
||||
}
|
||||
|
||||
const onboarding = localStorage.getItem('onboarding')
|
||||
|
||||
const steps = [
|
||||
{
|
||||
element: '#step-1',
|
||||
popover: {
|
||||
title: t('Tooltip-start-title'),
|
||||
description: t('Tooltip-start-content'),
|
||||
side: 'top',
|
||||
align: 'start'
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '#step-2',
|
||||
popover: {
|
||||
title: t('Tooltip-sound-title'),
|
||||
description: t('Tooltip-sound-content'),
|
||||
side: 'top',
|
||||
align: 'start'
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '#step-3',
|
||||
popover: {
|
||||
title: t('Tooltip-music-title'),
|
||||
description: t('Tooltip-music-content'),
|
||||
side: 'top',
|
||||
align: 'start'
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '#step-4',
|
||||
popover: {
|
||||
title: t('Tooltip-settings-title'),
|
||||
description: t('Tooltip-settings-content'),
|
||||
side: 'top',
|
||||
align: 'start'
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '#step-5',
|
||||
popover: {
|
||||
title: t('Tooltip-timer-title'),
|
||||
description: t('Tooltip-timer-content'),
|
||||
side: 'top',
|
||||
align: 'start'
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '#step-6',
|
||||
popover: {
|
||||
title: t('Let’s Go!'),
|
||||
description: t('We have saved your selection.') + '</br>' + t('You can change your settings at any time.') + '</br>' + '<img src="/images/rocketman.svg" height="35" class="img " alt="image">',
|
||||
align: 'center',
|
||||
popoverClass: 'onboarding-popover--last'
|
||||
}
|
||||
}
|
||||
]
|
||||
// localstorage abgreifen und wenn onboarding null, dann starten
|
||||
if (!userStore.isOnboardingCompleted) {
|
||||
onMounted(() => {
|
||||
const driverObj = driver({
|
||||
popoverClass: 'onboarding-popover',
|
||||
prevBtnText: '<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10.707 1.636a1 1 0 0 1 0 1.414L5.757 8l4.95 4.95a1 1 0 0 1-1.414 1.414L3.636 8.707a1 1 0 0 1 0-1.414l5.657-5.657a1 1 0 0 1 1.414 0Z" fill="#585C5E"/></svg>',
|
||||
nextBtnText: '<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M5.293 1.636a1 1 0 0 0 0 1.414L10.243 8l-4.95 4.95a1 1 0 0 0 1.414 1.414l5.657-5.657a1 1 0 0 0 0-1.414L6.707 1.636a1 1 0 0 0-1.414 0Z" fill="#585C5E"/></svg>',
|
||||
doneBtnText: t('Let’s Go!'),
|
||||
steps,
|
||||
onDestroyed: async () => {
|
||||
audioStore.setPlaying(true)
|
||||
|
||||
// Backend updaten
|
||||
try {
|
||||
await $axios.post('/api/account/onboarding-completed')
|
||||
userStore.markOnboardingCompleted() // Store aktualisieren
|
||||
} catch (error) {
|
||||
logger.error('Error updating onboarding status:', error)
|
||||
}
|
||||
|
||||
driverObj.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
driverObj.drive()
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
t,
|
||||
localePath,
|
||||
timer,
|
||||
progressBarWidth,
|
||||
progressColor,
|
||||
handleValueChange
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
activeNav: 'soundscape',
|
||||
activeDropdown: null,
|
||||
userStore: useUserStore(),
|
||||
form: {
|
||||
soundscape: '',
|
||||
headphone_type: '',
|
||||
anc_type: '',
|
||||
adaptive_sound_scape: ''
|
||||
},
|
||||
bar_width: 230,
|
||||
analyser: null,
|
||||
dataArray: null,
|
||||
bar_val: 25,
|
||||
soundscapes: [
|
||||
{ id: 0, title: 'Lagoon', src: window.location.origin + useRuntimeConfig().public.tracks.lagoon_src },
|
||||
{ id: 1, title: 'Tropics', src: window.location.origin + useRuntimeConfig().public.tracks.tropics_src },
|
||||
{ id: 2, title: 'Forest', src: window.location.origin + useRuntimeConfig().public.tracks.forest_src },
|
||||
{ id: 3, title: 'Meadow', src: window.location.origin + useRuntimeConfig().public.tracks.meadow_src }
|
||||
]
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.updateActiveSoundscape()
|
||||
getSettings()
|
||||
|
||||
const timerActivated = localStorage.getItem('timerActivated')
|
||||
if (timerActivated !== null) {
|
||||
const timer = useTimerStore()
|
||||
timer.activated = JSON.parse(timerActivated)
|
||||
}
|
||||
|
||||
document.addEventListener('click', this.handleClickOutside)
|
||||
},
|
||||
beforeUnmount () {
|
||||
document.removeEventListener('click', this.handleClickOutside)
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useUserStore, ['updateHeadsetType', 'updateANC', 'logout', 'soundscape']),
|
||||
...mapActions(useAudioStore, ['isPlaying', 'setPlaying']),
|
||||
updateMusicVolume (volume) {
|
||||
const musicGain = this.$refs[this.currentSoundscape]
|
||||
musicGain.updateMusicGain(volume)
|
||||
},
|
||||
handleAdaptiveSoundscapeChange (newValue) {
|
||||
this.adaptiveSoundscape = newValue
|
||||
this.saveSetting(newValue, 'adaptivesoundscape')
|
||||
useNuxtApp().$logger.log('Adaptive soundscape setting saved lets change the index page')
|
||||
},
|
||||
saveSettings (value, key) {
|
||||
useNuxtApp().$logger.log(`Saving ${key}: ${value}`)
|
||||
},
|
||||
updateActiveSoundscape () {
|
||||
switch (this.currentSoundscape) {
|
||||
case 'Lagoon':
|
||||
document.getElementById('lagoonLinkHomebar').classList.add('active')
|
||||
break
|
||||
case 'Tropics':
|
||||
document.getElementById('tropicsLinkHomebar').classList.add('active')
|
||||
break
|
||||
case 'Forest':
|
||||
document.getElementById('forestLinkHomebar').classList.add('active')
|
||||
break
|
||||
case 'Meadow':
|
||||
document.getElementById('meadowLinkHomebar').classList.add('active')
|
||||
break
|
||||
}
|
||||
},
|
||||
removeActiveSoundscape () {
|
||||
// foreach nav__dropdown-listItem remove active class
|
||||
const soundscapeLinks = document.querySelectorAll('.nav__dropdown-listItem')
|
||||
soundscapeLinks.forEach((link) => {
|
||||
link.classList.remove('active')
|
||||
})
|
||||
},
|
||||
playSound () {
|
||||
this.setPlaying(!this.isPlaying())
|
||||
},
|
||||
toggleDropdown (navItem) {
|
||||
this.activeDropdown = this.activeDropdown === navItem ? null : navItem
|
||||
},
|
||||
handleClickOutside (event) {
|
||||
if (
|
||||
this.$refs.soundscape &&
|
||||
!this.$refs.soundscape.contains(event.target) &&
|
||||
this.$refs.music &&
|
||||
!this.$refs.music.contains(event.target)
|
||||
) {
|
||||
this.activeDropdown = null
|
||||
}
|
||||
},
|
||||
setSoundMode (mode) {
|
||||
if (this.userStore.soundMode !== mode) {
|
||||
this.userStore.soundMode = mode
|
||||
|
||||
if (mode === 'soundscape') {
|
||||
this.$emit('update:soundscape', this.form)
|
||||
}
|
||||
}
|
||||
},
|
||||
saveSetting (value, type) {
|
||||
if (type === 'soundscape') {
|
||||
this.form.soundscape = value
|
||||
this.$emit('update:soundscape', this.form)
|
||||
|
||||
// Finde die passende Soundscape id
|
||||
const soundscape = this.soundscapes.find(s => s.title === value)
|
||||
|
||||
// remove active state from all soundscapes before nuxt link does its automatic magic
|
||||
this.removeActiveSoundscape()
|
||||
|
||||
if (soundscape) {
|
||||
this.$emit('update:currentIndex', soundscape.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style style="scss" scoped>
|
||||
|
||||
.nav {
|
||||
position: fixed;
|
||||
bottom: 3em;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
padding-right: 0;
|
||||
z-index: -1;
|
||||
|
||||
@media only screen and (max-width: 576px) {
|
||||
bottom: 1.5em;
|
||||
}
|
||||
|
||||
.nav__list {
|
||||
box-shadow: 0 0 14px 0 rgba(0, 0, 0, 0.1);
|
||||
-webkit-box-shadow: 0 0 14px 0 rgba(0, 0, 0, 0.1);
|
||||
-moz-box-shadow: 0 0 14px 0 rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 auto;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
border-radius: 6px;
|
||||
background-color: white;
|
||||
flex-basis: 100%;
|
||||
max-width: 350px;
|
||||
|
||||
@media only screen and (max-width: 576px) {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.nav__item {
|
||||
height: 50px;
|
||||
padding: 0.25em;
|
||||
flex: 1;
|
||||
|
||||
&:hover:not(.nav__item--active) {
|
||||
color: #e9c046;
|
||||
|
||||
/* .nav__dropdown {
|
||||
display: block;
|
||||
} */
|
||||
}
|
||||
|
||||
&:first-of-type {
|
||||
border-top-left-radius: 6px;
|
||||
border-bottom-left-radius: 6px;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
border-top-right-radius: 6px;
|
||||
border-bottom-right-radius: 6px;
|
||||
}
|
||||
|
||||
&.nav__item--toggle {
|
||||
cursor: pointer;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
|
||||
&:first-of-type {
|
||||
margin-left: 0.25em;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
&.nav__item--active {
|
||||
|
||||
.nav__item-link {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.nav__item-link {
|
||||
transition: background-color 0.25s, box-shadow 0.25s, color 0.25s;
|
||||
background-color: #F4F5F8;
|
||||
box-shadow: inset 0 0 3px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
&.nav__item--timer-bg {
|
||||
padding: 0;
|
||||
border-left: 2px solid #F4F5F8;
|
||||
overflow: hidden;
|
||||
|
||||
.nav__item-link {
|
||||
position: relative;
|
||||
|
||||
svg {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: calc(100% - var(--progress-width));
|
||||
transition: right 0.25s ease-in-out;
|
||||
bottom: 0;
|
||||
background-color: var(--progress-color);
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav__item-link {
|
||||
color: #585C5E;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: 250ms ease-in-out;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
color: #e9c046;
|
||||
}
|
||||
}
|
||||
|
||||
.nav__item-link--timer {
|
||||
&:hover {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav__dropdown{
|
||||
background-color: #F4F5F8;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
padding: 0.75em 1em 4em 1em;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: none;
|
||||
list-style: none;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 0 14px 0 rgba(0, 0, 0, 0.1);
|
||||
-webkit-box-shadow: 0 0 14px 0 rgba(0, 0, 0, 0.1);
|
||||
-moz-box-shadow: 0 0 14px 0 rgba(0, 0, 0, 0.1);
|
||||
z-index: -1;
|
||||
|
||||
&.nav__dropdown--no-padding {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
&.open {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav__dropdown-listItem {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 0.5em 1em;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
color: #585C5E;
|
||||
transition: 250ms ease-in-out;
|
||||
|
||||
&.active, &.is-select {
|
||||
background-color: #e9c046;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #e9c046;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
p{
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.form-switch {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1em;
|
||||
padding-left: 0;
|
||||
|
||||
.form-switch__label {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
268
components/PageHeader.vue
Normal file
@@ -0,0 +1,268 @@
|
||||
<template>
|
||||
<header class="header d-flex flex-row flex-fill justify-content-between pt-3">
|
||||
<div class="header__logo">
|
||||
<nuxt-link class="navbar-brand" to="/">
|
||||
<div class="text-start">
|
||||
<img src="/mindboostlogo.svg" height="35" class="img " alt="imae">
|
||||
</div>
|
||||
</nuxt-link>
|
||||
</div>
|
||||
<div class="header__title d-none d-md-block">
|
||||
<transition name="fade-title" mode="out-in">
|
||||
<h1
|
||||
v-if="user.soundMode === 'music'"
|
||||
id="musicMode"
|
||||
:key="'music'"
|
||||
class="text-decoration-none fs-3 fw-bolder text-white"
|
||||
aria-current="page"
|
||||
>
|
||||
<span class="header__icon mx-1">
|
||||
<svg width="26" height="29" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#a)"><path d="M8.286 23.875c0 .829-.331 1.624-.92 2.21a3.152 3.152 0 0 1-4.445 0 3.116 3.116 0 0 1 0-4.42 3.152 3.152 0 0 1 4.444 0c.59.586.92 1.381.92 2.21Zm0 0V5.645L24 2v18.23M8.286 11.895 24 8.25m0 12.5c0 .829-.331 1.624-.92 2.21a3.152 3.152 0 0 1-4.445 0 3.116 3.116 0 0 1 0-4.42 3.152 3.152 0 0 1 4.444 0c.59.586.921 1.381.921 2.21Z" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" /></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h26v29H0z" /></clipPath></defs></svg>
|
||||
</span>
|
||||
{{ t('musicmode') }}
|
||||
</h1>
|
||||
|
||||
<h1
|
||||
v-else
|
||||
id="soundscapeTitle"
|
||||
:key="title"
|
||||
class="text-decoration-none fs-3 fw-bolder text-white"
|
||||
aria-current="page"
|
||||
>
|
||||
{{ title ? t(title) : '' }}
|
||||
<span class="header__icon">
|
||||
<component :is="currentSoundscapeSVG" />
|
||||
</span>
|
||||
</h1>
|
||||
</transition>
|
||||
</div>
|
||||
<nav class="header__nav">
|
||||
<SideBar />
|
||||
</nav>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { markRaw } from 'vue'
|
||||
import { mapActions } from 'pinia'
|
||||
import WaveTitleSVG from './svg/sounds/WaveTitleSvg.vue'
|
||||
import ForestTitleSVG from './svg/sounds/ForestTitleSvg.vue'
|
||||
import TropicsTitleSVG from './svg/sounds/TropicsTitleSvg.vue'
|
||||
import MeadowTitleSVG from './svg/sounds/MeadowTitleSvg.vue'
|
||||
import SideBar from './SideBar.vue'
|
||||
import { useUserStore } from '~/stores/user'
|
||||
|
||||
export default {
|
||||
name: 'PageHeader',
|
||||
components: {
|
||||
WaveTitleSVG,
|
||||
TropicsTitleSVG,
|
||||
ForestTitleSVG,
|
||||
MeadowTitleSVG,
|
||||
SideBar
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup () {
|
||||
const { t } = useI18n()
|
||||
const user = useUserStore()
|
||||
return { t, user }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
currentSoundscapeSVG: markRaw(WaveTitleSVG)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
title (newTitle) {
|
||||
this.updateCurrentSoundscapeSVG(newTitle)
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.updateCurrentSoundscapeSVG(this.title)
|
||||
},
|
||||
methods: {
|
||||
updateCurrentSoundscapeSVG (title) {
|
||||
switch (title) {
|
||||
case 'Lagoon':
|
||||
this.currentSoundscapeSVG = markRaw(WaveTitleSVG)
|
||||
break
|
||||
case 'Tropics':
|
||||
this.currentSoundscapeSVG = markRaw(TropicsTitleSVG)
|
||||
break
|
||||
case 'Forest':
|
||||
this.currentSoundscapeSVG = markRaw(ForestTitleSVG)
|
||||
break
|
||||
case 'Meadow':
|
||||
this.currentSoundscapeSVG = markRaw(MeadowTitleSVG)
|
||||
break
|
||||
default:
|
||||
this.currentSoundscapeSVG = markRaw(WaveTitleSVG)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.fade-title-enter-active,
|
||||
.fade-title-leave-active {
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
.fade-title-enter-from,
|
||||
.fade-title-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
.header {
|
||||
align-items: center;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 1.5em;
|
||||
}
|
||||
.header__logo{
|
||||
width: 30%;
|
||||
}
|
||||
.header__nav {
|
||||
width: 30%;
|
||||
max-width: unset;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.header__title {
|
||||
position: relative;
|
||||
color: white;
|
||||
}
|
||||
.header__title h1 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.header__icon {
|
||||
position: absolute;
|
||||
left: -1.75em;
|
||||
top: 40%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.nav-icons{
|
||||
border-radius: 10px;
|
||||
padding: 10px 10px;
|
||||
}
|
||||
.nav-icons:focus{
|
||||
background-color: #e9c046;
|
||||
color: white !important;
|
||||
width: 100% !important;
|
||||
border-radius: 10px;
|
||||
fill: white;
|
||||
}
|
||||
.nav-icons:focus svg path {
|
||||
fill: white;
|
||||
}
|
||||
|
||||
.fivith-icon.router-link-exact-active{
|
||||
content: url('~/assets/image/Vector (4).png');
|
||||
background-color: #e9c046;
|
||||
border-radius: 5px;
|
||||
padding: 10px 10px;
|
||||
text-align: center;
|
||||
}
|
||||
.accordion-button-homebar::after {
|
||||
background-size: 16px;
|
||||
background-position-y: 4px;
|
||||
}
|
||||
|
||||
.accordion-button {
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.accordion-item .nav-link {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.navbar-logo{
|
||||
width: 160px !important;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
}
|
||||
.drop-active, .drop-select{
|
||||
color: white !important;
|
||||
background-color: #e9c046;
|
||||
}
|
||||
|
||||
.home-dropdown li::after{
|
||||
/* content: ''; */
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
display: block;
|
||||
width: 87%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.home-dropdown li:last-of-type::after{
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
border-radius: 0;
|
||||
transition: 150ms ease-in-out;
|
||||
}
|
||||
|
||||
.dropdown-active svg{
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.navbar-controls {
|
||||
max-width: 250px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.navbar-controls .nav {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.navbar-controls {
|
||||
position: fixed;
|
||||
bottom: 1.5em;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.navbar-controls .nav {
|
||||
float: unset;
|
||||
background-color: white !important;
|
||||
}
|
||||
|
||||
.adaptive {
|
||||
padding-bottom: 5em !important;
|
||||
}
|
||||
|
||||
.header__logo{
|
||||
width: auto;
|
||||
}
|
||||
.header__nav {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 576px) {
|
||||
.navbar-controls {
|
||||
width: 80%;
|
||||
max-width: unset;
|
||||
}
|
||||
|
||||
.navbar-controls .nav {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.header__logo img {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
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
@@ -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
@@ -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
@@ -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
|
268
components/Players/ThePlayer.vue
Normal file
@@ -0,0 +1,268 @@
|
||||
<template>
|
||||
<div v-show="false" class="soundscape-wrapper">
|
||||
<!-- Main Audio Processing Components -->
|
||||
<div v-if="userStore.soundMode !== 'music'">
|
||||
<MusicGain id="music" ref="musicGain" :src="src" :title="currentSoundscape" />
|
||||
</div>
|
||||
<div v-if="adaptive">
|
||||
<AdaptiveNoiseGain id="noise" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<NoiseGain id="noise" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, computed } from 'vue'
|
||||
|
||||
import AdaptiveNoiseGain from '../experiments/homepages/AdaptiveNoiseGain.vue'
|
||||
import MusicGain from '../experiments/homepages/MusicGain.vue'
|
||||
import NoiseGain from '../experiments/homepages/NoiseGain.vue'
|
||||
|
||||
import { usePlayerControls } from '~/composables/usePlayerControls'
|
||||
import { useAudioReplacer } from '~/composables/useAudioReplacer'
|
||||
import { useUserStore } from '~/stores/user'
|
||||
import { useAudioStore } from '~/stores/audio'
|
||||
|
||||
import tracksConfig, { getSoundscapeByTitle } from '~/tracks.config'
|
||||
|
||||
// Refs
|
||||
const musicGain = ref(null) as any
|
||||
|
||||
// Store & Composables
|
||||
const userStore = useUserStore()
|
||||
const { addSpaceListener, addMediaControls, getNextSoundscape } = usePlayerControls()
|
||||
const { getAudioElementForTitle } = useMediaProvider()
|
||||
const { fadeVolume, replaceAudioWithFade } = useAudioReplacer()
|
||||
const artworkManager = useArtWorkManager()
|
||||
|
||||
const defaultSoundscape = 'Lagoon'
|
||||
const currentSoundscape = ref(useUserStore().user.settings.soundscape)
|
||||
|
||||
// Current state if the user wishes adaptive adaptions
|
||||
const adaptive = computed(() => userStore.user?.settings?.adaptive_sound_scape || 'no')
|
||||
const src = computed(() => getSoundscapeByTitle(currentSoundscape.value))
|
||||
|
||||
// Watch for changes in the selected soundscape
|
||||
// Current soundscape (reactive)
|
||||
|
||||
const replaceSrc = () => {
|
||||
const { replaceAudioWithFade } = useAudioReplacer()
|
||||
const title = getNextSoundscape()
|
||||
const newSrc = getSoundscapeByTitle(title)
|
||||
const duration = 5
|
||||
replaceAudioWithFade(title, newSrc, 5)
|
||||
}
|
||||
|
||||
// Mount logic: Register keyboard and media controls
|
||||
onMounted(() => {
|
||||
addSpaceListener()
|
||||
addMediaControls()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style style="scss" scoped>
|
||||
|
||||
.nav {
|
||||
position: fixed;
|
||||
bottom: 3em;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
padding-right: 0;
|
||||
z-index: -1;
|
||||
|
||||
@media only screen and (max-width: 576px) {
|
||||
bottom: 1.5em;
|
||||
}
|
||||
|
||||
.nav__list {
|
||||
box-shadow: 0 0 14px 0 rgba(0, 0, 0, 0.1);
|
||||
-webkit-box-shadow: 0 0 14px 0 rgba(0, 0, 0, 0.1);
|
||||
-moz-box-shadow: 0 0 14px 0 rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 auto;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
border-radius: 6px;
|
||||
background-color: white;
|
||||
flex-basis: 100%;
|
||||
max-width: 350px;
|
||||
|
||||
@media only screen and (max-width: 576px) {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.nav__item {
|
||||
height: 50px;
|
||||
padding: 0.25em;
|
||||
flex: 1;
|
||||
|
||||
&:hover:not(.nav__item--active) {
|
||||
color: #e9c046;
|
||||
|
||||
/* .nav__dropdown {
|
||||
display: block;
|
||||
} */
|
||||
}
|
||||
|
||||
&:first-of-type {
|
||||
border-top-left-radius: 6px;
|
||||
border-bottom-left-radius: 6px;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
border-top-right-radius: 6px;
|
||||
border-bottom-right-radius: 6px;
|
||||
}
|
||||
|
||||
&.nav__item--toggle {
|
||||
cursor: pointer;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
|
||||
&:first-of-type {
|
||||
margin-left: 0.25em;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
&.nav__item--active {
|
||||
|
||||
.nav__item-link {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.nav__item-link {
|
||||
transition: background-color 0.25s, box-shadow 0.25s, color 0.25s;
|
||||
background-color: #F4F5F8;
|
||||
box-shadow: inset 0 0 3px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
&.nav__item--timer-bg {
|
||||
padding: 0;
|
||||
border-left: 2px solid #F4F5F8;
|
||||
overflow: hidden;
|
||||
|
||||
.nav__item-link {
|
||||
position: relative;
|
||||
|
||||
svg {
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: calc(100% - var(--progress-width));
|
||||
transition: right 0.25s ease-in-out;
|
||||
bottom: 0;
|
||||
background-color: var(--progress-color);
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav__item-link {
|
||||
color: #585C5E;
|
||||
cursor: pointer;
|
||||
transition: 250ms ease-in-out;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
|
||||
&:hover {
|
||||
color: #e9c046;
|
||||
}
|
||||
|
||||
&.nav__item-link--play {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.nav__item-link--timer {
|
||||
&:hover {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav__dropdown{
|
||||
background-color: #F4F5F8;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
padding: 0.75em 1em 4em 1em;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: none;
|
||||
list-style: none;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 0 14px 0 rgba(0, 0, 0, 0.1);
|
||||
-webkit-box-shadow: 0 0 14px 0 rgba(0, 0, 0, 0.1);
|
||||
-moz-box-shadow: 0 0 14px 0 rgba(0, 0, 0, 0.1);
|
||||
z-index: -1;
|
||||
|
||||
&.nav__dropdown--no-padding {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
&.open {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav__dropdown-listItem {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 0.5em 1em;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
color: #585C5E;
|
||||
transition: 250ms ease-in-out;
|
||||
|
||||
&.active, &.is-select {
|
||||
background-color: #e9c046;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #e9c046;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
p{
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.form-switch {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1em;
|
||||
padding-left: 0;
|
||||
|
||||
.form-switch__label {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
0
components/Players/adaptivePlayer.vue
Normal file
288
components/SideBar.vue
Normal file
@@ -0,0 +1,288 @@
|
||||
<template>
|
||||
<button
|
||||
class="sidebar__menu-icon text-white"
|
||||
:aria-expanded="isOpen.toString()"
|
||||
aria-controls="sidebar"
|
||||
:aria-label="t('Toggle Sidebar')"
|
||||
@click.stop="toggleSidebar"
|
||||
>
|
||||
<span v-if="!isOpen"><svg width="28" height="18" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M2 18c-.425 0-.781-.144-1.068-.432A1.459 1.459 0 0 1 .5 16.5c-.001-.424.143-.78.432-1.068A1.454 1.454 0 0 1 2 15h24c.425 0 .782.144 1.07.432.288.288.431.644.43 1.068-.001.424-.145.78-.432 1.07A1.435 1.435 0 0 1 26 18H2Zm0-7.5c-.425 0-.781-.144-1.068-.432A1.459 1.459 0 0 1 .5 9c-.001-.424.143-.78.432-1.068A1.454 1.454 0 0 1 2 7.5h24c.425 0 .782.144 1.07.432.288.288.431.644.43 1.068-.001.424-.145.78-.432 1.07A1.435 1.435 0 0 1 26 10.5H2ZM2 3c-.425 0-.781-.144-1.068-.432A1.459 1.459 0 0 1 .5 1.5C.499 1.076.643.72.932.432A1.454 1.454 0 0 1 2 0h24c.425 0 .782.144 1.07.432.288.288.431.644.43 1.068-.001.424-.145.78-.432 1.07A1.435 1.435 0 0 1 26 3H2Z" fill="currentColor" /></svg></span>
|
||||
<span v-else><svg width="36" height="36" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="m18 20.1-7.35 7.35c-.275.275-.625.413-1.05.413-.425 0-.775-.138-1.05-.413-.275-.275-.412-.625-.412-1.05 0-.425.137-.775.412-1.05L15.9 18l-7.35-7.35c-.275-.275-.412-.625-.412-1.05 0-.425.137-.775.412-1.05.275-.275.625-.413 1.05-.413.425 0 .775.138 1.05.413L18 15.9l7.35-7.35c.275-.275.625-.413 1.05-.413.425 0 .775.138 1.05.413.275.275.413.625.413 1.05 0 .425-.138.775-.413 1.05L20.1 18l7.35 7.35c.275.275.413.625.413 1.05 0 .425-.138.775-.413 1.05-.275.275-.625.413-1.05.413-.425 0-.775-.138-1.05-.413L18 20.1Z" fill="#585C5E" /></svg></span>
|
||||
</button>
|
||||
<div
|
||||
ref="sidebarContainer"
|
||||
:class="['sidebar', { 'sidebar--open': isOpen }]
|
||||
"
|
||||
role="navigation"
|
||||
aria-label="Hauptnavigation"
|
||||
:aria-hidden="(!isOpen).toString()"
|
||||
>
|
||||
<ul class="sidebar__main-menu">
|
||||
<li class="sidebar__item">
|
||||
<nuxt-link
|
||||
:to="localePath('/settings/account')"
|
||||
class="sidebar__link"
|
||||
:tabindex="isOpen ? 0 : -1"
|
||||
:aria-hidden="!isOpen"
|
||||
>
|
||||
<i class="fa-solid fa-user sidebar__icon" /><span class="sidebar__text">{{ t('Account') }}</span>
|
||||
</nuxt-link>
|
||||
</li>
|
||||
<li v-if="dataStore.user.team_subscription_plan == '0' && dataStore.user.enterprise_subscription_plan == '0' && (dataStore.user.basic_subscription_plan == '1')" class="sidebar__item">
|
||||
<nuxt-link :to="localePath('/settings/subscription')" class="sidebar__link" :tabindex="isOpen ? 0 : -1">
|
||||
<i class="fa-solid fa-credit-card sidebar__icon" /><span class="sidebar__text">{{ t('Subscription') }}</span>
|
||||
</nuxt-link>
|
||||
</li>
|
||||
<li class="sidebar__item">
|
||||
<nuxt-link
|
||||
:to="localePath('/settings/soundscape')"
|
||||
class="sidebar__link"
|
||||
:tabindex="isOpen ? 0 : -1"
|
||||
:aria-hidden="!isOpen"
|
||||
>
|
||||
<i class="fa-solid fa-volume-high sidebar__icon" /><span class="sidebar__text">{{ t('Settings') }}</span>
|
||||
</nuxt-link>
|
||||
</li>
|
||||
<li class="sidebar__item">
|
||||
<nuxt-link
|
||||
:to="localePath('/settings/about')"
|
||||
class="sidebar__link"
|
||||
:tabindex="isOpen ? 0 : -1"
|
||||
:aria-hidden="!isOpen"
|
||||
>
|
||||
<i class="fa-solid fa-circle-info sidebar__icon" /><span class="sidebar__text"> {{ t('About Mindboost') }}</span>
|
||||
</nuxt-link>
|
||||
</li>
|
||||
<li class="sidebar__item">
|
||||
<nuxt-link
|
||||
:to="localePath('/settings/faq')"
|
||||
class="sidebar__link"
|
||||
:tabindex="isOpen ? 0 : -1"
|
||||
:aria-hidden="!isOpen"
|
||||
>
|
||||
<i class="fa-solid fa-circle-question sidebar__icon" /><span class="sidebar__text">{{ t('FAQ') }} </span>
|
||||
</nuxt-link>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="sidebar__secondary-menu">
|
||||
<li class="sidebar__item">
|
||||
<nuxt-link
|
||||
:to="localePath('/settings/imprint')"
|
||||
class="sidebar__link sidebar__link--secondary"
|
||||
:tabindex="isOpen ? 0 : -1"
|
||||
:aria-hidden="!isOpen"
|
||||
>
|
||||
<i class="fa-solid fa-stamp sidebar__icon" /><span class="sidebar__text">{{ t('Imprint') }} </span>
|
||||
</nuxt-link>
|
||||
</li>
|
||||
|
||||
<li class="sidebar__item">
|
||||
<nuxt-link
|
||||
:to="localePath('/settings/dataprotection')"
|
||||
class="sidebar__link sidebar__link--secondary"
|
||||
:tabindex="isOpen ? 0 : -1"
|
||||
:aria-hidden="!isOpen"
|
||||
>
|
||||
<i class="fa-solid fa-shield sidebar__icon" /><span class="sidebar__text">{{ t('Data Protection') }}</span>
|
||||
</nuxt-link>
|
||||
</li>
|
||||
|
||||
<li class="sidebar__item">
|
||||
<nuxt-link
|
||||
:to="localePath('/settings/termsandcondition')"
|
||||
class="sidebar__link sidebar__link--secondary"
|
||||
:tabindex="isOpen ? 0 : -1"
|
||||
:aria-hidden="!isOpen"
|
||||
@blur="handleBlur"
|
||||
>
|
||||
<i class="fa-solid fa-scale-balanced sidebar__icon" /><span class="sidebar__text">{{ t('Terms & Conditions') }}</span>
|
||||
</nuxt-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { onMounted, onBeforeUnmount, ref } from 'vue'
|
||||
import { useUserStore } from '~/stores/user'
|
||||
|
||||
export default {
|
||||
setup () {
|
||||
const isOpen = ref(false)
|
||||
const sidebarContainer = ref(null)
|
||||
const dataStore = useUserStore()
|
||||
const { t } = useI18n()
|
||||
const localePath = useLocalePath()
|
||||
|
||||
const toggleSidebar = () => {
|
||||
isOpen.value = !isOpen.value
|
||||
}
|
||||
|
||||
const handleClickOutside = (event) => {
|
||||
if (sidebarContainer.value && !sidebarContainer.value.contains(event.target)) {
|
||||
isOpen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleBlur = () => {
|
||||
// Timeout nötig, um neuen Fokus zu berücksichtigen
|
||||
setTimeout(() => {
|
||||
const active = document.activeElement
|
||||
if (
|
||||
isOpen.value &&
|
||||
sidebarContainer.value &&
|
||||
!sidebarContainer.value.contains(active)
|
||||
) {
|
||||
isOpen.value = false
|
||||
}
|
||||
}, 10)
|
||||
}
|
||||
|
||||
const handleEscape = (e) => {
|
||||
if (e.key === 'Escape' && isOpen.value) {
|
||||
isOpen.value = false
|
||||
document.querySelector('.sidebar__menu-icon')?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
document.addEventListener('keydown', handleEscape)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
document.removeEventListener('keydown', handleEscape)
|
||||
})
|
||||
|
||||
return { t, localePath, dataStore, isOpen, toggleSidebar, sidebarContainer, handleBlur }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: -350px;
|
||||
width: 350px;
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
transition: right 0.3s ease;
|
||||
z-index: 1000;
|
||||
padding: 6em 0em 2em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
&--open {
|
||||
box-shadow: -2px 0 10px rgba(0, 0, 0, 0.2);
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&.sidebar--settings {
|
||||
background-color: rgb(244, 245, 247);
|
||||
position: unset;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-top: 0;
|
||||
|
||||
.sidebar__item {
|
||||
&::after {
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__menu-icon {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
&__main-menu,
|
||||
&__secondary-menu {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__item {
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 1em;
|
||||
right: 1em;
|
||||
bottom: 0;
|
||||
height: 2px;
|
||||
background-color: #f3f3f3;
|
||||
transition: 250ms ease-in-out;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&::after {
|
||||
background-color: #e9c046;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
&::after {
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar__link {
|
||||
text-decoration: none;
|
||||
color: #585C5E;
|
||||
font-size: 1.2em;
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0.75em 1em;
|
||||
font-weight: 700;
|
||||
transition: 250ms ease-in-out;
|
||||
|
||||
&:hover {
|
||||
background-color: #e9c046;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&--secondary {
|
||||
font-size: 0.9em;
|
||||
color: #85878C;
|
||||
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
color: #85878C;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.sidebar__icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&__secondary-menu {
|
||||
.sidebar__item {
|
||||
&::after{
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.sidebar--settings {
|
||||
|
||||
.sidebar__icon {
|
||||
|
||||
@media only screen and (max-width: 576px) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
313
components/experiments/AudioElement.vue
Normal file
@@ -0,0 +1,313 @@
|
||||
<template>
|
||||
<div class="slider-wrapper">
|
||||
<!-- Slot for passing any audio element -->
|
||||
<slot />
|
||||
<div class="slider">
|
||||
<input
|
||||
id="gain-control"
|
||||
v-model.number="volumeValue"
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.001"
|
||||
data-toggle="tooltip"
|
||||
data-placement="top"
|
||||
:title="tooltipTitle"
|
||||
@wheel.prevent="changeVolumeOnWheel"
|
||||
>
|
||||
<span
|
||||
class="slider-progress-bar"
|
||||
:style="{ width: `${volumeValue * 100}%` }"
|
||||
/>
|
||||
</div>
|
||||
<audio
|
||||
:id="title"
|
||||
ref="audioElement"
|
||||
hidden
|
||||
loop
|
||||
@loadedmetadata="handleUpdatedMetadata"
|
||||
@canplay="ready=true"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import { useAudioStore } from '~/stores/audio'
|
||||
import { useUserStore } from '~/stores/user'
|
||||
|
||||
export default {
|
||||
name: 'AudioElement',
|
||||
props: {
|
||||
src: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: 'Unknown'
|
||||
},
|
||||
tooltipTitle: {
|
||||
type: String,
|
||||
default: 'Change the volume by click, scroll or touch.',
|
||||
required: false
|
||||
},
|
||||
volume: {
|
||||
type: Number,
|
||||
default: 1
|
||||
}
|
||||
},
|
||||
emits: ['update:volume', 'update:loaded', 'update:playing', 'update:canplay'],
|
||||
setup (props, { emit }) {
|
||||
const audioStore = useAudioStore()
|
||||
const audioElement = ref<HTMLAudioElement | null>(null)
|
||||
const volumeValue = ref(props.volume)
|
||||
const ready = ref(false)
|
||||
|
||||
const mute = () => {
|
||||
if ((audioElement.value instanceof HTMLAudioElement)) {
|
||||
const ae = audioElement.value
|
||||
ae.muted = true
|
||||
}
|
||||
}
|
||||
const play = async () => {
|
||||
try {
|
||||
const { $audioPrepared } = useNuxtApp()
|
||||
await $audioPrepared
|
||||
const sink = useUserStore().audioOutputDevice as MediaDeviceInfo
|
||||
audioElement.value?.setSinkId(sink.deviceId)
|
||||
if (audioElement.value instanceof HTMLAudioElement) {
|
||||
// set audio element to mute, will be anyway muted after connecting to the web audio graph
|
||||
audioElement.value.play()
|
||||
}
|
||||
} catch (error) {
|
||||
useNuxtApp().$logger.error('Oouh sorry! Error while setting up audio, please reload.', error)
|
||||
}
|
||||
}
|
||||
|
||||
const pause = () => {
|
||||
audioElement.value?.pause()
|
||||
}
|
||||
|
||||
const togglePlayPause = async () => {
|
||||
if (audioElement.value) {
|
||||
if (audioStore.playing) {
|
||||
await audioElement.value.play()
|
||||
} else {
|
||||
audioElement.value.pause()
|
||||
}
|
||||
}
|
||||
}
|
||||
watch(() => props.volume, (newValue) => {
|
||||
volumeValue.value = newValue
|
||||
})
|
||||
watch(() => audioStore.playing, async (newValue) => {
|
||||
if (newValue) {
|
||||
updateMediaSession()
|
||||
await togglePlayPause()
|
||||
} else {
|
||||
await togglePlayPause()
|
||||
}
|
||||
})
|
||||
|
||||
watch(volumeValue, (newValue) => {
|
||||
if (audioElement.value) {
|
||||
audioElement.value.volume = newValue
|
||||
emit('update:volume', newValue) // Gib die Änderungen an die Elternkomponente weiter
|
||||
}
|
||||
})
|
||||
|
||||
const changeVolumeOnWheel = (event:WheelEvent) => {
|
||||
// Adjust volume on wheel scroll
|
||||
const deltaY = event.deltaY
|
||||
if (deltaY < 0) {
|
||||
const volumeAdd = (Math.min(1, volumeValue.value + 0.02))
|
||||
volumeValue.value = volumeAdd
|
||||
} else {
|
||||
const volumeCut = (Math.max(0, volumeValue.value - 0.02))
|
||||
volumeValue.value = volumeCut
|
||||
}
|
||||
}
|
||||
|
||||
const updateMediaSession = () => {
|
||||
try {
|
||||
const path = window.location.origin + '/images/scenery/' + props.title.toLowerCase() + '.jpg'
|
||||
// useNuxtApp().$logger.log(navigator.mediaSession.metadata)
|
||||
if (audioElement.value) {
|
||||
audioElement.value.controls = true
|
||||
navigator.mediaSession.metadata = new MediaMetadata({
|
||||
title: props.title,
|
||||
artist: 'mindboost',
|
||||
album: 'mindboost Originale',
|
||||
artwork: [
|
||||
{ src: path, sizes: '192x192', type: 'image/jpeg' }
|
||||
]
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
// useNuxtApp().$logger.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdatedMetadata = () => {
|
||||
// use the navigator for the soundscape.
|
||||
if (props.title !== 'Noise') {
|
||||
updateMediaSession()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (audioElement.value) {
|
||||
audioElement.value.src = props.src
|
||||
audioElement.value.volume = volumeValue.value
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
audioElement.value = null
|
||||
volumeValue.value = 0
|
||||
})
|
||||
|
||||
return {
|
||||
audioElement,
|
||||
ready,
|
||||
play,
|
||||
pause,
|
||||
handleUpdatedMetadata,
|
||||
volumeValue,
|
||||
togglePlayPause,
|
||||
changeVolumeOnWheel,
|
||||
updateMediaSession
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.slider-wrapper{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin: 1em auto;
|
||||
}
|
||||
|
||||
.slider-icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 576px) {
|
||||
.slider{
|
||||
max-width: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
.slider-progress-bar {
|
||||
position: absolute;
|
||||
height: 10px;
|
||||
background-color: #e9c046;
|
||||
border-top-left-radius: 5px;
|
||||
border-bottom-left-radius: 5px;
|
||||
border-radius: 5px;
|
||||
left: 0;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* Allgemeine Einstellungen für den Slider */
|
||||
input[type="range"] {
|
||||
-webkit-appearance: none; /* Entfernt das Standard-Styling in Webkit-Browsern */
|
||||
appearance: none; /* Entfernt Standard-Styling in anderen Browsern */
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
background: #fff;
|
||||
border-radius: 5px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
z-index:1;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
/* Styling für den Track in Webkit-Browsern */
|
||||
input[type="range"]::-webkit-slider-runnable-track {
|
||||
height: 10px;
|
||||
background: transparent;
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* Styling für den Thumb in Webkit-Browsern */
|
||||
input[type="range"]::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
margin-top: -5px;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
border: 2px solid #e9c046;
|
||||
}
|
||||
|
||||
/* Styling für den Track in Firefox */
|
||||
input[type="range"]::-moz-range-track {
|
||||
height: 10px;
|
||||
background: transparent;
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Styling für den Thumb in Firefox */
|
||||
input[type="range"]::-moz-range-thumb {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
background-color: #e9c046;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
border: 2px solid #e9c046;
|
||||
margin-top: -5px;
|
||||
}
|
||||
|
||||
input[type="range"]::-moz-range-progress {
|
||||
height: 10px;
|
||||
background-color: #e9c046;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
/* Styling für den Track in Internet Explorer/Edge */
|
||||
input[type="range"]::-ms-track {
|
||||
height: 10px;
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
color: transparent;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
/* Styling für den Thumb in Internet Explorer/Edge */
|
||||
input[type="range"]::-ms-thumb {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
background: #f5f5f5;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 3px rgba(0, 0, 0, 0.3);
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
border: 2px solid #e9c046;
|
||||
}
|
||||
|
||||
/* Entfernt Ticks in IE */
|
||||
input[type="range"]::-ms-ticks-after,
|
||||
input[type="range"]::-ms-ticks-before {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
285
components/experiments/AudioElement2.vue
Normal file
@@ -0,0 +1,285 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- Slot for passing any audio element -->
|
||||
<slot />
|
||||
<div class="slider">
|
||||
<input
|
||||
id="gain-control"
|
||||
v-model="volume"
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.02"
|
||||
@wheel.prevent="changeVolumeOnWheel"
|
||||
>
|
||||
</div>
|
||||
<audio
|
||||
ref="audioElement"
|
||||
hidden
|
||||
autoplay
|
||||
muted
|
||||
loop
|
||||
@play="handlePlay"
|
||||
@pause="handlePause"
|
||||
@keydown="handleKeyDown"
|
||||
@loadedmetadata="handleLoaded"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import { useAudioStore } from '~/stores/audio'
|
||||
import { useUserStore } from '~/stores/user'
|
||||
|
||||
export default {
|
||||
name: 'AudioElement',
|
||||
props: {
|
||||
src: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: 'Unknown'
|
||||
}
|
||||
},
|
||||
emits: ['update:volume', 'update:loaded', 'update:playing', 'update:fadeout'],
|
||||
setup (props, { emit }) {
|
||||
const audioStore = useAudioStore()
|
||||
const audioElement = ref<HTMLAudioElement | null>(null)
|
||||
const volume = ref(1)
|
||||
|
||||
const play = () => {
|
||||
try {
|
||||
// set audio element to mute, will be anyway muted after connecting to the web audio graph
|
||||
// useNuxtApp().$logger.log('Trigger Play of the audioelement tag: CurrentTime ' + currentTime + ' paused' + paused + ' ended' + ended)
|
||||
const sink = useUserStore().audioOutputDevice as MediaDeviceInfo
|
||||
audioElement.value?.setSinkId(sink.deviceId)
|
||||
audioElement.value?.play()
|
||||
if (audioElement.value) { audioElement.value.muted = true }
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
const pause = () => {
|
||||
audioElement.value?.pause()
|
||||
}
|
||||
|
||||
const pauseFadeOut = () => {
|
||||
// useNuxtApp().$logger.log('pauseFadeOut')
|
||||
emit('update:fadeout')
|
||||
setTimeout(() => {
|
||||
pause()
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
const isPlaying = () => {
|
||||
return audioStore.playing
|
||||
}
|
||||
|
||||
onUpdated(() => {
|
||||
if (props.title === 'Noise') {
|
||||
// useNuxtApp().$logger.log('cannot set control of noise to false')
|
||||
} else if ('mediaSession' in navigator) {
|
||||
const path = window.location.origin + '/images/scenery/' + props.title.toLowerCase() + '.jpg'
|
||||
navigator.mediaSession.metadata = new MediaMetadata({
|
||||
title: props.title,
|
||||
artist: 'mindboost',
|
||||
album: 'mindboost Originale',
|
||||
artwork: [
|
||||
{ src: path, sizes: '192x192', type: 'image/jpeg' }
|
||||
]
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => audioStore.playing, (newValue) => {
|
||||
if (newValue) {
|
||||
// useNuxtApp().$logger.log('PLAY THE AUDIO')
|
||||
play()
|
||||
} else {
|
||||
// useNuxtApp().$logger.log('PAUSE THE AUDIO')
|
||||
pauseFadeOut()
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => volume.value, (newValue) => {
|
||||
if (audioElement.value) {
|
||||
emit('update:volume', newValue)
|
||||
}
|
||||
})
|
||||
|
||||
const changeVolumeOnWheel = (event:WheelEvent) => {
|
||||
// Adjust volume on wheel scroll
|
||||
const deltaY = event.deltaY
|
||||
if (deltaY < 0) {
|
||||
const volumeAdd = (Math.min(1, volume.value + 0.02))
|
||||
volume.value = volumeAdd
|
||||
} else {
|
||||
const volumeCut = (Math.max(0, volume.value - 0.02))
|
||||
volume.value = volumeCut
|
||||
}
|
||||
}
|
||||
|
||||
let enableKeyHandler = true
|
||||
|
||||
const debounce = <T extends (...args: any[]) => void>(func: T, timeout: number = 1500): () => void => {
|
||||
let timer: NodeJS.Timeout | null
|
||||
return () => {
|
||||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
timer = setTimeout(() => { func.apply(this) }, timeout)
|
||||
}
|
||||
}
|
||||
|
||||
const reEnableKeyHandler = debounce(() => {
|
||||
enableKeyHandler = true
|
||||
})
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (!enableKeyHandler) { return }
|
||||
if (e.code === 'Space') {
|
||||
e.preventDefault() // Prevent the default action (scrolling)
|
||||
// useNuxtApp().$logger.log('The current state is ' + audioStore.playing + ' so start or stop audio')
|
||||
if (audioStore.playing) {
|
||||
handlePause()
|
||||
} else {
|
||||
handlePlay()
|
||||
}
|
||||
enableKeyHandler = false // Disable handler
|
||||
reEnableKeyHandler()
|
||||
}
|
||||
}
|
||||
|
||||
const handlePlay = () => {
|
||||
// useNuxtApp().$logger.log('handlePlay called ' + audioStore.isPlaying())
|
||||
// First handlePlay is triggered by the autoplay, so no need to start play
|
||||
emit('update:playing', true)
|
||||
audioStore.setPlaying(true)
|
||||
// useNuxtApp().$logger.log('handlePlay ended with ' + audioStore.isPlaying())
|
||||
}
|
||||
|
||||
const handlePause = () => {
|
||||
// useNuxtApp().$logger.log('handlePause: Change play state to false')
|
||||
emit('update:playing', false)
|
||||
audioStore.playing = false
|
||||
}
|
||||
|
||||
const handleLoaded = () => {
|
||||
emit('update:loaded', true)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (audioElement.value) {
|
||||
audioElement.value.src = props.src
|
||||
if (props.title !== 'Noise') {
|
||||
window.addEventListener('keydown', handleKeyDown.bind(this))
|
||||
if ('mediaSession' in navigator) {
|
||||
// Play action
|
||||
navigator.mediaSession.setActionHandler('play', (_e) => {
|
||||
handlePlay()
|
||||
// if (!enableMediaHandler) {
|
||||
/// / useNuxtApp().$logger.log('during event handling - being busy ')
|
||||
// return
|
||||
// }
|
||||
// enableMediaHandler = false // Disable handler
|
||||
// reEnableMediaHandler()
|
||||
// // Your play action here
|
||||
})
|
||||
|
||||
// Stop action
|
||||
navigator.mediaSession.setActionHandler('stop', (_e) => {
|
||||
// if (!enableMediaHandler) { return }
|
||||
// // Your stop action here
|
||||
// if (audioElement.value) {
|
||||
// audioElement.value.currentTime = 0
|
||||
// audioElement.value.src = ''
|
||||
// audioElement.value.removeAttribute('src')
|
||||
// }
|
||||
// audioStore.playing = false
|
||||
})
|
||||
|
||||
// Pause action
|
||||
navigator.mediaSession.setActionHandler('pause', (_e) => {
|
||||
handlePause()
|
||||
// if (!enableMediaHandler) {
|
||||
/// / useNuxtApp().$logger.log('during event handling - being busy ')
|
||||
// return
|
||||
// }
|
||||
// audioStore.playing = false
|
||||
// enableMediaHandler = false // Disable handler
|
||||
// reEnableMediaHandler()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onRenderTriggered(() => {
|
||||
// useNuxtApp().$logger.log('render AudioElement-----------' + props.title)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('keydown', handleKeyDown.bind(this))
|
||||
})
|
||||
|
||||
return {
|
||||
audioElement,
|
||||
play,
|
||||
pause,
|
||||
isPlaying,
|
||||
handlePlay,
|
||||
handlePause,
|
||||
handleLoaded,
|
||||
handleKeyDown,
|
||||
volume,
|
||||
changeVolumeOnWheel
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.slider {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
/* Style adjustments if needed */
|
||||
input[type="range"] {
|
||||
background:transparent !important;
|
||||
}
|
||||
/* Styles the track */
|
||||
input[type="range"]::-webkit-slider-runnable-track {
|
||||
background: #e9c046; /* yellow track */
|
||||
height: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
input[type="range"]::-moz-range-track {
|
||||
background: #e9c046; /* yellow track */
|
||||
height: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
input[type="range"]::-ms-track {
|
||||
background: #e9c046; /* yellow track */
|
||||
border-color: transparent;
|
||||
color: transparent;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
/* Styles the thumb */
|
||||
input[type="range"]::-webkit-slider-thumb {
|
||||
-webkit-appearance: none; /* Required to style in Webkit browsers */
|
||||
margin-top: -6px; /* Adjusts the position of the thumb relative to the track */
|
||||
}
|
||||
|
||||
input[type="range"]::-moz-range-thumb {
|
||||
border: none; /* Removes any default border */
|
||||
}
|
||||
|
||||
input[type="range"]::-ms-thumb {
|
||||
margin-top: 0; /* May need to adjust the position similar to webkit */
|
||||
border: none; /* Removes any default border */
|
||||
}
|
||||
</style>
|
224
components/experiments/AudioTag.vue
Normal file
@@ -0,0 +1,224 @@
|
||||
<template>
|
||||
<h1>AUDiO Tag mit dem volume: {{ safeVolume }}</h1>
|
||||
<p>audioStatus {{ audioStatus }}</p>
|
||||
<button @click="muteAudio">muteAudio</button>
|
||||
<audio
|
||||
v-if="src"
|
||||
ref="audioElement"
|
||||
:src="src"
|
||||
loop="true"
|
||||
:volume="safeVolume"
|
||||
mediagroup="noiseGroup"
|
||||
sinkId=""
|
||||
@canplay="()=> $emit('canplay')"
|
||||
/>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import type { Logger } from 'pino'
|
||||
import { defineComponent, ref, watch, computed } from 'vue'
|
||||
import { ensureAudio, useAudioStore } from '~/stores/audio'
|
||||
import { useUserStore } from '~/stores/user'
|
||||
export default defineComponent({
|
||||
name: 'AudioTag',
|
||||
props: {
|
||||
src: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
volume: {
|
||||
type: [Number, String],
|
||||
default: 0.23,
|
||||
validator: (value:any) => {
|
||||
const num = parseFloat(value)
|
||||
return num >= -3.4 && num <= 3.4
|
||||
}
|
||||
},
|
||||
play: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['canplay'],
|
||||
setup (props, { emit: $emit }) {
|
||||
const logger = useNuxtApp().$logger as Logger
|
||||
const audioElement = ref<HTMLAudioElement | null>(null)
|
||||
const audioStatus = ref<'stopped' | 'playing' | 'paused'>('stopped')
|
||||
|
||||
const safeVolume = computed(() => {
|
||||
const vol = parseFloat(props.volume.toString())
|
||||
return Math.max(0, Math.min(1, isNaN(vol) ? 1 : vol))
|
||||
})
|
||||
|
||||
const muteAudio = () => {
|
||||
if (audioElement.value) {
|
||||
audioElement.value.muted = !audioElement.value.muted
|
||||
}
|
||||
}
|
||||
|
||||
// kleine Hilfsfunktion für Volume-Fading
|
||||
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)
|
||||
}
|
||||
|
||||
const startPlaying = async () => {
|
||||
const audioStore = useAudioStore()
|
||||
await ensureAudio()
|
||||
const sink = useUserStore().audioOutputDevice as MediaDeviceInfo
|
||||
audioElement.value?.setSinkId(sink.deviceId)
|
||||
if (audioElement.value && audioStore.isPlaying()) {
|
||||
try {
|
||||
const audio = audioElement.value
|
||||
audio.currentTime = 0
|
||||
audio.volume = 0
|
||||
audio.muted = false
|
||||
|
||||
await audio.play().catch((error) => {
|
||||
logger.warn('Error playing audio:', error)
|
||||
})
|
||||
|
||||
fadeVolume(audio, 0, 0.3, 8000) // weiches Fade-In
|
||||
audioStatus.value = 'playing'
|
||||
} catch (error) {
|
||||
logger.warn('Error starting audio:', error)
|
||||
}
|
||||
} else {
|
||||
logger.info('Audio Playback has not started, audioStore is not playing')
|
||||
}
|
||||
}
|
||||
|
||||
const pausePlaying = () => {
|
||||
if (audioElement.value && audioStatus.value === 'playing') {
|
||||
try {
|
||||
const audio = audioElement.value
|
||||
const initialVolume = audio.volume
|
||||
|
||||
fadeVolume(audio, initialVolume, 0, 300)
|
||||
|
||||
setTimeout(() => {
|
||||
audio.pause()
|
||||
audioStatus.value = 'paused'
|
||||
}, 350) // minimal länger als Fade
|
||||
} catch (error) {
|
||||
logger.warn('Error pausing audio:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const stopAndResetPlaying = () => {
|
||||
if (audioElement.value) {
|
||||
try {
|
||||
const audio = audioElement.value
|
||||
const sink = useUserStore().audioOutputDevice as MediaDeviceInfo
|
||||
audio.setSinkId(sink.deviceId)
|
||||
audio.pause()
|
||||
audio.currentTime = 0
|
||||
audio.volume = safeVolume.value
|
||||
audio.muted = true
|
||||
audioStatus.value = 'stopped'
|
||||
} catch (error) {
|
||||
logger.warn('Error stopping audio:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const resumePlaying = async () => {
|
||||
const audioStore = useAudioStore()
|
||||
await ensureAudio()
|
||||
|
||||
if (audioElement.value && audioStore.isPlaying() && audioStatus.value === 'paused') {
|
||||
try {
|
||||
const audio = audioElement.value
|
||||
audio.muted = false
|
||||
audio.volume = 0
|
||||
|
||||
await audio.play().catch((error) => {
|
||||
logger.warn('Error resuming audio:', error)
|
||||
})
|
||||
|
||||
fadeVolume(audio, 0, safeVolume.value, 500) // kürzeres Fade-In
|
||||
audioStatus.value = 'playing'
|
||||
} catch (error) {
|
||||
logger.warn('Error resuming audio:', error)
|
||||
}
|
||||
} else {
|
||||
logger.info('Audio Resume not possible: wrong audio state')
|
||||
}
|
||||
}
|
||||
|
||||
const stopPlaying = () => {
|
||||
if (audioElement.value) {
|
||||
try {
|
||||
const audio = audioElement.value
|
||||
const initialVolume = audio.volume
|
||||
|
||||
fadeVolume(audio, initialVolume, 0, 500)
|
||||
|
||||
setTimeout(() => {
|
||||
audio.pause()
|
||||
audio.currentTime = 0
|
||||
audio.volume = safeVolume.value
|
||||
audio.muted = true
|
||||
audioStatus.value = 'stopped'
|
||||
}, 550)
|
||||
} catch (error) {
|
||||
logger.warn('Error stopping and fading audio:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.src, () => {
|
||||
stopAndResetPlaying()
|
||||
if (audioElement.value) {
|
||||
audioElement.value.load()
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => props.play, (newPlay) => {
|
||||
setTimeout(() => {
|
||||
if (newPlay === false) {
|
||||
pausePlaying()
|
||||
} else if (audioStatus.value === 'paused') {
|
||||
resumePlaying()
|
||||
} else {
|
||||
startPlaying()
|
||||
}
|
||||
}, 500)
|
||||
})
|
||||
|
||||
watch(() => safeVolume.value, (newVolume) => {
|
||||
if (audioElement.value && audioStatus.value === 'playing') {
|
||||
try {
|
||||
audioElement.value.volume = newVolume
|
||||
} catch (error) {
|
||||
logger.warn('Error updating audio volume:', error)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
audioElement,
|
||||
startPlaying,
|
||||
pausePlaying,
|
||||
stopPlaying,
|
||||
resumePlaying,
|
||||
safeVolume,
|
||||
muteAudio,
|
||||
audioStatus
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
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>
|
96
components/experiments/AudioTagWebAudioStreaming.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<h3> AudioTagWebAudio </h3>
|
||||
<p>In diesem Test funktioniert die Messung der db</p>
|
||||
<button @click="fetchAudio">Fetch </button> <br> fetched:{{ readyState }}<br><br>
|
||||
<button v-if="readyState" @click="startAudio"> Play </button> gain:<p v-if="gainNode"> {{ gainNode }} </p>
|
||||
<div v-if="audioNodes.length > 0">
|
||||
<!-- eslint-disable-next-line vue/require-v-for-key -->
|
||||
<div v-for="node in audioNodes">
|
||||
state: {{ node.state() }} <br>
|
||||
src: {{ node._src }} <br>
|
||||
playing: {{ node.playing() }} <br>
|
||||
webaudio: {{ node._webAudio }} <br>
|
||||
duration: {{ node.duration() }} <br>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, watch, computed } from 'vue'
|
||||
import { createStreamingAudioSource } from '~/lib/AudioFunctions'
|
||||
import { useAudioStore } from '~/stores/audio'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'AudioTagWebAudio',
|
||||
props: {
|
||||
src: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
volume: {
|
||||
type: Number,
|
||||
default: 0.5
|
||||
}
|
||||
},
|
||||
emits: ['canplay'],
|
||||
setup (props, { emit: $emit }) {
|
||||
const ctx = useAudioStore().getContext()
|
||||
const readyState = ref(false)
|
||||
const fadingState = ref(false)
|
||||
const audioNodes = ref([] as Array<Howl>)
|
||||
|
||||
const safeVolume = computed((): Number => {
|
||||
const volumeVal = props.volume as number
|
||||
if (volumeVal >= 0 && volumeVal <= 1) { return volumeVal }
|
||||
return Math.abs(volumeVal - 0) < Math.abs(volumeVal - 1) ? 0 : 1
|
||||
})
|
||||
|
||||
let gainNode: GainNode | null = null
|
||||
let audioElement: Howl | null = null
|
||||
|
||||
const emitReady = () => {
|
||||
$emit('canplay')
|
||||
}
|
||||
const connectGainNode = (source:AudioBufferSourceNode) => {
|
||||
gainNode = ctx.createGain()
|
||||
|
||||
gainNode.gain.setValueAtTime(0, ctx.currentTime)
|
||||
source.connect(gainNode)
|
||||
|
||||
gainNode.connect(ctx.destination)
|
||||
}
|
||||
const fetchAudio = async () => {
|
||||
audioElement = null
|
||||
useNuxtApp().$logger.log('PFAD: ' + props.src)
|
||||
audioElement = await createStreamingAudioSource(ctx, props.src)
|
||||
audioNodes.value.push(audioElement)
|
||||
if (audioElement instanceof Howl) {
|
||||
audioElement.volume(props.volume)
|
||||
audioElement?.play()
|
||||
useNuxtApp().$logger.log({ audioElement })
|
||||
readyState.value = true
|
||||
emitReady()
|
||||
}
|
||||
useNuxtApp().$logger.log('gefetchtes audioElement', { audioElement })
|
||||
}
|
||||
const startAudio = () => {
|
||||
useNuxtApp().$logger.log('start Audio')
|
||||
audioElement?.mute()
|
||||
}
|
||||
|
||||
watch(() => props.volume, () => {
|
||||
audioElement?.volume(props.volume)
|
||||
})
|
||||
|
||||
return {
|
||||
safeVolume,
|
||||
emitReady,
|
||||
fetchAudio,
|
||||
readyState,
|
||||
fadingState,
|
||||
startAudio,
|
||||
gainNode,
|
||||
audioNodes
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
101
components/experiments/AudioVisualization.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<div>
|
||||
<canvas id="audioVisualizer" width="640" height="100" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
function drawVisualization (analyser, canvasCtx) {
|
||||
const bufferLength = analyser.frequencyBinCount
|
||||
const dataArray = new Uint8Array(bufferLength)
|
||||
|
||||
const WIDTH = canvasCtx.canvas.width
|
||||
const HEIGHT = canvasCtx.canvas.height
|
||||
|
||||
function draw () {
|
||||
requestAnimationFrame(draw)
|
||||
|
||||
analyser.getByteTimeDomainData(dataArray)
|
||||
|
||||
canvasCtx.fillStyle = 'rgb(200, 200, 200)'
|
||||
canvasCtx.fillRect(0, 0, WIDTH, HEIGHT)
|
||||
|
||||
canvasCtx.lineWidth = 2
|
||||
canvasCtx.strokeStyle = 'rgb(0, 0, 0)'
|
||||
|
||||
canvasCtx.beginPath()
|
||||
|
||||
const sliceWidth = WIDTH * 1.0 / bufferLength
|
||||
let x = 0
|
||||
|
||||
for (let i = 0; i < bufferLength; i++) {
|
||||
const v = dataArray[i] / 128.0
|
||||
const y = v * HEIGHT / 2
|
||||
|
||||
if (i === 0) {
|
||||
canvasCtx.moveTo(x, y)
|
||||
} else {
|
||||
canvasCtx.lineTo(x, y)
|
||||
}
|
||||
|
||||
x += sliceWidth
|
||||
}
|
||||
|
||||
canvasCtx.lineTo(WIDTH, HEIGHT / 2)
|
||||
canvasCtx.stroke()
|
||||
}
|
||||
|
||||
draw()
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'AudioVisualization',
|
||||
props: {
|
||||
analyser: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
setup (props) {
|
||||
onMounted(() => {
|
||||
const audioCanvas = document.getElementById('audioVisualizer').getContext('2d')
|
||||
drawVisualization(props.analyser, audioCanvas)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('keydown', handleKeyDown.bind(this))
|
||||
})
|
||||
|
||||
return {
|
||||
audioCanvas: null
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Style adjustments if needed */
|
||||
/* Styles the track */
|
||||
input[type="range"]::-webkit-slider-runnable-track {
|
||||
background: #e9c046; /* yellow track */
|
||||
height: 8px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
input[type="range"]::-moz-range-track {
|
||||
background: #e9c046; /* yellow track */
|
||||
height: 8px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
input[type="range"]::-ms-track {
|
||||
background: #e9c046; /* yellow track */
|
||||
border-color: transparent;
|
||||
color: transparent;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
</style>
|
||||
^
|
169
components/experiments/GainController.vue
Normal file
@@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="error" class="error-message">
|
||||
{{ error }}
|
||||
</div>
|
||||
<AudioFileSelector @file-selected="onFileSelected" />
|
||||
<!-- Ramp Time Control -->
|
||||
<div class="ramp-time-control">
|
||||
<label for="rampTime">Ramp Time (ms):</label>
|
||||
<input
|
||||
id="rampTime"
|
||||
v-model="rampTime"
|
||||
type="range"
|
||||
min="100"
|
||||
max="50000"
|
||||
step="100"
|
||||
>
|
||||
<span>{{ rampTime }}ms</span>
|
||||
</div>
|
||||
<div class="gain-controller">
|
||||
<div v-for="frequency in frequencies" :key="frequency" class="frequency-control">
|
||||
<RNBOControlValue
|
||||
:center-frequency="frequency"
|
||||
@control-value-change="(value) => handleValueChange(value.frequency, value.value)"
|
||||
/>
|
||||
<AudioTagWebAudio
|
||||
:ref="el => { if (el) audioElements[frequency] = el }"
|
||||
:src="audioSrc"
|
||||
:volume="currentVolumes[frequency]"
|
||||
/>
|
||||
<div>
|
||||
Frequency: {{ frequency }}Hz
|
||||
<br>Gain Value (dB): {{ gainValuesDB[frequency] }}
|
||||
<br>Normalized: {{ normalizedVolumes[frequency] }}
|
||||
<br>Volume: {{ currentVolumes[frequency] }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, onUnmounted, reactive } from 'vue'
|
||||
import AudioFileSelector from '../AudioFileSelector.vue'
|
||||
import AudioTagWebAudio from './AudioTagWebAudio.vue'
|
||||
import RNBOControlValue from './tests/ControlValues/RNBOControlValue.vue'
|
||||
import { useAudioStore } from '~/stores/audio'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'GainController',
|
||||
components: {
|
||||
RNBOControlValue,
|
||||
AudioTagWebAudio,
|
||||
AudioFileSelector
|
||||
},
|
||||
setup () {
|
||||
const logger = useNuxtApp().$logger
|
||||
logger.info('GainController setup')
|
||||
const audioStore = useAudioStore()
|
||||
logger.info('Got audioStore', audioStore)
|
||||
const audioSrc = ref(window.location.origin + '/sounds/lagoon.ogg')
|
||||
const rampTime = ref(25000)
|
||||
logger.info('Set rampTime', 25000)
|
||||
const frequencies = ref([63, 125, 250, 500, 1000, 2000, 4000, 8000, 16000])
|
||||
logger.info('Set frequencies', { frequencies })
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
const gainValuesDB = reactive<Record<number, number>>({})
|
||||
const currentVolumes = reactive<Record<number, number>>({})
|
||||
const normalizedVolumes = reactive<Record<number, number>>({})
|
||||
const audioElements = reactive<Record<number, InstanceType<typeof AudioTagWebAudio>>>({})
|
||||
|
||||
const rampIntervals: Record<number, ReturnType<typeof setInterval> | null> = {}
|
||||
|
||||
const onFileSelected = (file: string) => {
|
||||
const fullPath = new URL(file, window.location.origin).toString()
|
||||
audioSrc.value = fullPath
|
||||
logger.info('User hat ein File ausgewählt ' + file)
|
||||
useNuxtApp().$logger.log({ audioElements })
|
||||
setTimeout(() => {
|
||||
audioStore.setPlaying(true)
|
||||
}, 250)
|
||||
}
|
||||
|
||||
const calculateNormalizedVolume = (db: number): number => {
|
||||
const minDB = -12
|
||||
const maxDB = 2
|
||||
return (db - minDB) / (maxDB - minDB)
|
||||
}
|
||||
|
||||
const rampVolume = (frequency: number, targetVolume: number, duration: number) => {
|
||||
const startVolume = currentVolumes[frequency] || 1
|
||||
const startTime = Date.now()
|
||||
const endTime = startTime + duration
|
||||
|
||||
if (rampIntervals[frequency]) {
|
||||
clearInterval(rampIntervals[frequency]!)
|
||||
}
|
||||
|
||||
rampIntervals[frequency] = setInterval(() => {
|
||||
const now = Date.now()
|
||||
if (now >= endTime) {
|
||||
currentVolumes[frequency] = targetVolume
|
||||
clearInterval(rampIntervals[frequency]!)
|
||||
rampIntervals[frequency] = null
|
||||
return
|
||||
}
|
||||
|
||||
const progress = (now - startTime) / duration
|
||||
currentVolumes[frequency] = startVolume + (targetVolume - startVolume) * progress
|
||||
}, 50)
|
||||
}
|
||||
|
||||
const handleValueChange = (frequency: number, value: number) => {
|
||||
logger.info('Change for ' + frequency + ' to ' + value)
|
||||
if (!isNaN(value)) {
|
||||
gainValuesDB[frequency] = value
|
||||
normalizedVolumes[frequency] = calculateNormalizedVolume(value)
|
||||
rampVolume(frequency, normalizedVolumes[frequency], rampTime.value)
|
||||
} else {
|
||||
useNuxtApp().$logger.log('value is not NaN', { value })
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
Object.values(rampIntervals).forEach((interval) => {
|
||||
if (interval) { clearInterval(interval) }
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
frequencies,
|
||||
gainValuesDB,
|
||||
currentVolumes,
|
||||
normalizedVolumes,
|
||||
audioElements,
|
||||
handleValueChange,
|
||||
error,
|
||||
onFileSelected,
|
||||
audioSrc,
|
||||
rampTime
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.error-message {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
padding: 10px;
|
||||
border: 1px solid red;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.ramp-time-control {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.ramp-time-control input {
|
||||
width: 300px;
|
||||
margin: 0 10px;
|
||||
}
|
||||
.frequency-control {
|
||||
margin-bottom: 20px;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
}
|
||||
</style>
|
173
components/experiments/TotalGainController.vue
Normal file
@@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="error" class="error-message">
|
||||
{{ error }}
|
||||
</div>
|
||||
<AudioFileSelector @file-selected="onFileSelected" />
|
||||
<button @click="startAll">Start all Noises</button>
|
||||
<!-- Ramp Time Control -->
|
||||
</div>
|
||||
<div class="gain-controller">
|
||||
<div v-for="frequency in frequencies" :key="frequency" class="frequency-control">
|
||||
<RNBOControlValue
|
||||
:v-if="!isNaN(frequency)"
|
||||
:center-frequency="frequency"
|
||||
:status="audioSources[frequency] != null"
|
||||
@control-value-change="(value) => handleValueChange(value.frequency, value.value)"
|
||||
/>
|
||||
<div :v-if="audioSources[frequency] != undefined && audioSources[frequency] != ''">
|
||||
<AudioTagWebAudio
|
||||
:ref="el => { if (el) audioElements[frequency] = el as any}"
|
||||
:src="audioSources[frequency] || ''"
|
||||
:volume="Number(currentVolumes[frequency])"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
Frequency: {{ frequency }}Hz
|
||||
<br>Gain Value (dB): {{ gainValuesDB[frequency] }}
|
||||
<br>Normalized: {{ normalizedVolumes[frequency] }}
|
||||
<br>Volume: {{ currentVolumes[frequency] }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, onUnmounted, reactive } from 'vue'
|
||||
import AudioFileSelector from '../AudioFileSelector.vue'
|
||||
import AudioTagWebAudio from './AudioTagWebAudio.vue'
|
||||
import RNBOControlValue from './tests/ControlValues/RNBOControlValue.vue'
|
||||
import { calculateNormalizedVolume } from '~/lib/AudioFunctions'
|
||||
import { useAudioStore } from '~/stores/audio'
|
||||
import tracksConfig from '~/tracks.config'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'GainController',
|
||||
components: {
|
||||
RNBOControlValue,
|
||||
AudioTagWebAudio,
|
||||
AudioFileSelector
|
||||
},
|
||||
setup () {
|
||||
const logger = useNuxtApp().$logger as any
|
||||
logger.info('GainController setup')
|
||||
const audioStore = useAudioStore()
|
||||
logger.info('Got audioStore', audioStore)
|
||||
const audioSrc = ref(window.location.origin + '/sounds/lagoon.ogg')
|
||||
const rampTime = ref(25000)
|
||||
logger.info('Set rampTime', 25000)
|
||||
const frequencies = ref([63, 125, 250, 500, 1000, 2000, 4000, 8000, 16000])
|
||||
logger.info('Set frequencies', { frequencies })
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
const gainValuesDB = reactive<Record<number, number>>({})
|
||||
const currentVolumes = reactive<Record<number, number>>({})
|
||||
const audioSources = reactive<Record<number, string>>({})
|
||||
const normalizedVolumes = reactive<Record<number, number>>({})
|
||||
const audioElements = reactive<Record<number, InstanceType<typeof AudioTagWebAudio>>>({})
|
||||
|
||||
const rampIntervals: Record<number, ReturnType<typeof setInterval> | null> = {}
|
||||
|
||||
const onFileSelected = (file: string) => {
|
||||
const fullPath = new URL(file, window.location.origin).toString()
|
||||
audioSrc.value = fullPath
|
||||
logger.info('User hat ein File ausgewählt ' + file)
|
||||
useNuxtApp().$logger.log({ audioElements })
|
||||
setTimeout(() => {
|
||||
audioStore.setPlaying(true)
|
||||
}, 250)
|
||||
}
|
||||
|
||||
const startAll = () => {
|
||||
const audioPaths = [tracksConfig['63_src'], tracksConfig['125_src'], tracksConfig['250_src'], tracksConfig['500_src'],
|
||||
tracksConfig['1000_src'], tracksConfig['2000_src'], tracksConfig['4000_src'], tracksConfig['8000_src'], tracksConfig['16000_src']]
|
||||
const recordsCount = Object.keys(audioElements).length
|
||||
if (recordsCount > 0) {
|
||||
for (let recordsCounter = 0; recordsCounter < recordsCount; recordsCounter++) {
|
||||
const audioElement = audioElements[frequencies.value[recordsCounter]]
|
||||
audioSources[frequencies.value[recordsCounter]] = `${window.location.origin}${encodeURI(audioPaths[recordsCounter])}`
|
||||
}
|
||||
useNuxtApp().$logger.log(audioSources[63])
|
||||
}
|
||||
}
|
||||
|
||||
const rampVolume = (frequency: number, targetVolume: number, duration: number) => {
|
||||
const startVolume = currentVolumes[frequency] || 1
|
||||
const startTime = Date.now()
|
||||
const endTime = startTime + duration
|
||||
|
||||
if (rampIntervals[frequency]) {
|
||||
clearInterval(rampIntervals[frequency]!)
|
||||
}
|
||||
|
||||
rampIntervals[frequency] = setInterval(() => {
|
||||
const now = Date.now()
|
||||
if (now >= endTime) {
|
||||
currentVolumes[frequency] = targetVolume
|
||||
clearInterval(rampIntervals[frequency]!)
|
||||
rampIntervals[frequency] = null
|
||||
return
|
||||
}
|
||||
|
||||
const progress = (now - startTime) / duration
|
||||
currentVolumes[frequency] = startVolume + (targetVolume - startVolume) * progress
|
||||
}, 50)
|
||||
}
|
||||
|
||||
const handleValueChange = (frequency: number, value: number) => {
|
||||
if (!isNaN(value)) {
|
||||
gainValuesDB[frequency] = value
|
||||
normalizedVolumes[frequency] = calculateNormalizedVolume(value)
|
||||
currentVolumes[frequency] = normalizedVolumes[frequency]
|
||||
} else {
|
||||
logger.warn('value is not a number, skip...', { value })
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
Object.values(rampIntervals).forEach((interval) => {
|
||||
if (interval) { clearInterval(interval) }
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
frequencies,
|
||||
gainValuesDB,
|
||||
currentVolumes,
|
||||
normalizedVolumes,
|
||||
audioElements,
|
||||
handleValueChange,
|
||||
error,
|
||||
onFileSelected,
|
||||
audioSrc,
|
||||
rampTime,
|
||||
startAll,
|
||||
audioSources
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.error-message {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
padding: 10px;
|
||||
border: 1px solid red;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.ramp-time-control {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.ramp-time-control input {
|
||||
width: 300px;
|
||||
margin: 0 10px;
|
||||
}
|
||||
.frequency-control {
|
||||
margin-bottom: 20px;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
}
|
||||
</style>
|
139
components/experiments/homepages/AdaptiveNoiseGain.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<div class="player">
|
||||
<NoiseControlledWebAudio3Band
|
||||
v-for="(frequency, index) in frequencies"
|
||||
ref="Player"
|
||||
:key="frequency"
|
||||
:master-attack="masterAttack"
|
||||
:master-release="masterRelease"
|
||||
:center-frequency="frequency"
|
||||
:master-gain="masterGain"
|
||||
:q-factor="qFactors[index]"
|
||||
@ready="onBandReady"
|
||||
@update:mid-volume="controlMusicGain"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
|
||||
import { useAudioStore } from '../../../stores/audio'
|
||||
import { useMicStore } from '~/stores/microphone'
|
||||
import type { Microphone } from '~/stores/interfaces/Microphone'
|
||||
import NoiseControlledWebAudio3Band from '~/components/experiments/tests/ControlValues/NoiseControlledWebAudio3Band.vue'
|
||||
|
||||
export default {
|
||||
name: 'AdaptiveNoiseGain',
|
||||
components: {
|
||||
NoiseControlledWebAudio3Band
|
||||
},
|
||||
emits: ['musicGain'],
|
||||
setup () {
|
||||
const masterGain = ref(useAudioStore().getMasterGainNoise())
|
||||
const player = ref(null)
|
||||
const { t } = useI18n()
|
||||
const frequencies = ref([150, 1500, 8000])
|
||||
const qFactors = ref([0.8, 0.9, 0.6])
|
||||
const loadedBands = ref(0)
|
||||
const muted = computed(() => useAudioStore().getNoiseVolume < 0.01)
|
||||
let oldVolume = 0
|
||||
|
||||
const route = useRoute()
|
||||
const isExperimentsRoute = computed(() => route.path.match(/\/[a-z]{2}\/experiments/))
|
||||
|
||||
const masterAttack = ref(120000 * 2) // Beispielwert in Samples
|
||||
const masterRelease = ref(144000 * 2)
|
||||
|
||||
const loading = computed(() => loadedBands.value < frequencies.value.length)
|
||||
|
||||
const onBandReady = () => {
|
||||
loadedBands.value++
|
||||
}
|
||||
const toggleMute = () => {
|
||||
if (!muted.value) {
|
||||
oldVolume = masterGain.value.gain.value
|
||||
masterGain.value.gain.linearRampToValueAtTime(0, masterGain.value.context.currentTime + 0.4)
|
||||
useAudioStore().setNoiseVolume(0)
|
||||
} else if (oldVolume > 0) {
|
||||
masterGain.value.gain.linearRampToValueAtTime(oldVolume, masterGain.value.context.currentTime + 0.4)
|
||||
useAudioStore().setNoiseVolume(oldVolume)
|
||||
} else {
|
||||
masterGain.value.gain.linearRampToValueAtTime(1, masterGain.value.context.currentTime + 0.4)
|
||||
useAudioStore().setNoiseVolume(1)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
frequencies,
|
||||
loading,
|
||||
onBandReady,
|
||||
t,
|
||||
loadedBands,
|
||||
masterAttack,
|
||||
masterRelease,
|
||||
isExperimentsRoute,
|
||||
qFactors,
|
||||
masterGain,
|
||||
toggleMute,
|
||||
muted,
|
||||
player
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
audioContext: useAudioStore().getContext(),
|
||||
musicReady: false,
|
||||
tropics_src: window.location.origin + useRuntimeConfig().public.tracks.masking_src as string,
|
||||
fading: false,
|
||||
connected: false,
|
||||
volume: useAudioStore().noiseVolume,
|
||||
previousVolume: useAudioStore().noiseVolume
|
||||
}
|
||||
},
|
||||
onMounted () {
|
||||
},
|
||||
watch: {
|
||||
volume (newVolume: number) {
|
||||
const audioStore = useAudioStore()
|
||||
audioStore.setNoiseVolume(newVolume)
|
||||
if (!isNaN(newVolume)) { audioStore.getMasterGainNoise().gain.linearRampToValueAtTime(newVolume, 0.125) }
|
||||
const m = this.muted
|
||||
}
|
||||
},
|
||||
beforeUnmount () {
|
||||
const micro = useMicStore().getMicrophone() as Microphone
|
||||
micro.microphoneStream?.getTracks().forEach(m => m.stop())
|
||||
},
|
||||
methods: {
|
||||
changeVolumeOnWheel (event:WheelEvent) {
|
||||
// Adjust volume on wheel scroll
|
||||
const gainValue = this.volume
|
||||
const deltaY = event.deltaY
|
||||
if (deltaY < 0) {
|
||||
const volumeAdd = (Math.min(1, gainValue + 0.02))
|
||||
|
||||
this.volume = volumeAdd
|
||||
} else {
|
||||
const volumeCut = (Math.max(0, gainValue - 0.02))
|
||||
this.volume = volumeCut
|
||||
}
|
||||
},
|
||||
controlMusicGain (value: string) {
|
||||
useAudioStore().setVolume(parseFloat(value))
|
||||
this.$emit('musicGain', value)
|
||||
},
|
||||
handleCanPlayNoise () {
|
||||
// useNuxtApp().$logger.log('NoiseElemeint has now playingstate: ' + state)
|
||||
this.musicReady = true
|
||||
},
|
||||
|
||||
readyForWebaudio () {
|
||||
if (!this.musicReady) {
|
||||
// useNuxtApp().$logger.log('music not ready')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
239
components/experiments/homepages/MusicGain.vue
Normal file
@@ -0,0 +1,239 @@
|
||||
<template>
|
||||
<div class="player">
|
||||
<AudioElement
|
||||
ref="Music"
|
||||
key="1"
|
||||
v-model:volume="volume"
|
||||
:src="src"
|
||||
:title="title"
|
||||
@update:playing="handlePlayingUpdate2"
|
||||
@update:canplay="handleCanPlayMusic"
|
||||
>
|
||||
<template #default="{ }">
|
||||
<img
|
||||
v-if="volume == 0"
|
||||
class="slider-icon"
|
||||
style="width: 25px; height: 25px;"
|
||||
src="~/assets/image/music_muted.svg"
|
||||
title="Click to unmute"
|
||||
@click="toggleMute()"
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
class="slider-icon"
|
||||
style="width: 25px; height: 25px;"
|
||||
src="~/assets/image/music.svg"
|
||||
title="Click to mute"
|
||||
@click="toggleMute()"
|
||||
>
|
||||
</template>
|
||||
</AudioElement>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import AudioElement from '../AudioElement.vue'
|
||||
import { useAudioStore } from '../../../stores/audio'
|
||||
|
||||
export default {
|
||||
name: 'MusicGain',
|
||||
components: { AudioElement },
|
||||
props: {
|
||||
src: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['musicReady'],
|
||||
data () {
|
||||
return {
|
||||
audioContext: useAudioStore().getContext(),
|
||||
createdNodes: {} as any,
|
||||
musicReady: false,
|
||||
fading: false,
|
||||
connected: false,
|
||||
muted: false,
|
||||
volume: useAudioStore().getVolume,
|
||||
previousVolume: useAudioStore().getVolume
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
musicReady (value) {
|
||||
this.$emit('musicReady', value)
|
||||
this.handlePlayingUpdate(true)
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.applyStoredVolume()
|
||||
},
|
||||
beforeUnmount () {
|
||||
this.disconnectNodes()
|
||||
},
|
||||
methods: {
|
||||
disconnectNodes () {
|
||||
if (typeof this.createdNodes === 'object' && this.createdNodes !== null) {
|
||||
Object.values(this.createdNodes).forEach((node) => {
|
||||
// Check if the node exists and has a disconnect method
|
||||
if (node && typeof AudioNode) {
|
||||
const tobedisconnected = node as AudioNode
|
||||
tobedisconnected.disconnect()
|
||||
node = null
|
||||
}
|
||||
})
|
||||
this.createdNodes = null
|
||||
}
|
||||
},
|
||||
toggleMute () {
|
||||
const element = this.$refs.Music as typeof AudioElement
|
||||
const audioElement = element.$refs.audioElement as HTMLMediaElement
|
||||
if (this.muted) {
|
||||
// Unmute: Stelle den vorherigen Lautstärkewert wieder her
|
||||
this.muted = false
|
||||
audioElement.muted = false
|
||||
this.volume = this.previousVolume || 1 // Falls kein vorheriger Wert gespeichert ist, setze auf 1
|
||||
audioElement.volume = this.volume
|
||||
} else {
|
||||
// Mute: Speichere den aktuellen Lautstärkewert und mute das Audio
|
||||
this.previousVolume = this.volume
|
||||
this.volume = 0
|
||||
audioElement.volume = 0
|
||||
this.muted = true
|
||||
audioElement.muted = true
|
||||
}
|
||||
useAudioStore().setVolume(this.volume)
|
||||
element.$emit('update:volume', this.volume)
|
||||
},
|
||||
mute () {
|
||||
const element = this.$refs.Music as typeof AudioElement
|
||||
const audioElement = element.$refs.audioElement as HTMLMediaElement
|
||||
audioElement.muted = true
|
||||
this.muted = audioElement.muted
|
||||
},
|
||||
unmute () {
|
||||
const element = this.$refs.Music as typeof AudioElement
|
||||
const audioElement = element.$refs.audioElement as HTMLMediaElement
|
||||
audioElement.muted = false
|
||||
this.muted = audioElement.muted
|
||||
},
|
||||
// This method helps to get the ressources free when we stop playing the audio
|
||||
// without it would be louder each time we start playing
|
||||
refreshAudioContext () {
|
||||
const newAudioContext = new AudioContext()
|
||||
this.audioContext.close()
|
||||
useAudioStore().audioContext = newAudioContext
|
||||
this.audioContext = useAudioStore().getContext()
|
||||
},
|
||||
fadeInGains () {
|
||||
if (useAudioStore().playing !== true) {
|
||||
logger.info('Skip interaction, because playing state is false.')
|
||||
} else {
|
||||
const element = this.$refs.Music as typeof AudioElement
|
||||
const audioElement = element.$refs.audioElement as HTMLMediaElement
|
||||
const fadeTime = this.audioContext.currentTime + 3.0
|
||||
this.fading = true
|
||||
this.unmute()
|
||||
audioElement.play()
|
||||
const musicGain = this.createdNodes.musicGain
|
||||
this.createdNodes.musicGain.gain.setValueAtTime(0, this.audioContext.currentTime)
|
||||
musicGain.gain.linearRampToValueAtTime(1.0, fadeTime)
|
||||
|
||||
setTimeout(() => {
|
||||
this.fading = false
|
||||
}, fadeTime * 1000)
|
||||
}
|
||||
},
|
||||
fadeOutGains () {
|
||||
if (this.createdNodes.musicGain) {
|
||||
const musicGainValue = this.createdNodes.musicGainValue.gain.value
|
||||
this.createdNodes.musicGain.gain.linearRampToValueAtTime(musicGainValue, this.audioContext.currentTime)
|
||||
this.createdNodes.musicGain.gain.linearRampToValueAtTime(0, this.audioContext.currentTime + 1.3)
|
||||
}
|
||||
},
|
||||
|
||||
handleCanPlayMusic () {
|
||||
this.musicReady = true
|
||||
this.handlePlayingUpdate(true)
|
||||
},
|
||||
|
||||
readyForWebaudio () {
|
||||
if (!this.musicReady) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
handlePlayingUpdate2 (state: boolean) {
|
||||
if (!state) {
|
||||
this.mute()
|
||||
return
|
||||
}
|
||||
if (this.readyForWebaudio()) {
|
||||
if (state) {
|
||||
this.handlePlayingUpdate(state)
|
||||
} else {
|
||||
this.fadeOutGains()
|
||||
}
|
||||
} else if (this.readyForWebaudio()) {
|
||||
this.handlePlayingUpdate(state)
|
||||
}
|
||||
},
|
||||
handlePlayingUpdate (state: boolean) {
|
||||
if (state) {
|
||||
const musicElement = this.$refs.Music as typeof AudioElement
|
||||
const musicAudioElement = musicElement.$refs.audioElement as HTMLMediaElement
|
||||
const audioContext = this.audioContext
|
||||
const destination = this.audioContext.destination
|
||||
|
||||
this.createdNodes.musicGain ||= audioContext.createGain()
|
||||
|
||||
this.createdNodes.musicGain.gain.setValueAtTime(0, audioContext.currentTime)
|
||||
|
||||
if (musicAudioElement.currentSrc !== this.src) {
|
||||
this.createdNodes.musicSource.disconnect()
|
||||
this.createdNodes.musicSource = audioContext.createMediaElementSource(musicAudioElement)
|
||||
this.createdNodes.musicSource.connect(this.createdNodes.musicGain)
|
||||
this.createdNodes.musicGain.connect(destination)
|
||||
}
|
||||
|
||||
this.createdNodes.musicSource ||= audioContext.createMediaElementSource(musicAudioElement)
|
||||
|
||||
this.createdNodes.musicSource.connect(this.createdNodes.musicGain)
|
||||
this.createdNodes.musicGain.connect(destination)
|
||||
this.createdNodes.musicGain.gain.cancelScheduledValues(this.audioContext.currentTime)
|
||||
this.createdNodes.musicGain.gain.setValueAtTime(0, this.audioContext.currentTime)
|
||||
|
||||
this.connected = true
|
||||
this.fadeInGains()
|
||||
// useAudioStore().playing = true
|
||||
} else {
|
||||
// Music has just stopped react on it.
|
||||
|
||||
this.fadeOutGains()
|
||||
this.createdNodes = []
|
||||
this.refreshAudioContext()
|
||||
this.connected = false
|
||||
}
|
||||
},
|
||||
applyStoredVolume () {
|
||||
const element = this.$refs.Music as typeof AudioElement
|
||||
const audioElement = element.$refs.audioElement as HTMLMediaElement
|
||||
|
||||
// Setze die Lautstärke des Audio-Elements
|
||||
audioElement.volume = this.volume
|
||||
|
||||
// Emitiere ein Event, um die Lautstärke in AudioElement zu aktualisieren
|
||||
element.$emit('update:volume', this.volume)
|
||||
},
|
||||
updateMusicGain (volume: number) {
|
||||
this.volume = volume // Lautstärke speichern
|
||||
useAudioStore().setVolume(volume)
|
||||
if (this.createdNodes.musicGain) {
|
||||
this.createdNodes.musicGain.gain.linearRampToValueAtTime(volume, this.createdNodes.musicGain.context.currentTime + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
180
components/experiments/homepages/MusicGainForest.vue
Normal file
@@ -0,0 +1,180 @@
|
||||
<template>
|
||||
<div class="player">
|
||||
<AudioElement
|
||||
ref="Music"
|
||||
key="1"
|
||||
:src="forest_src"
|
||||
title="Forest"
|
||||
@update:volume="updateMusicGain"
|
||||
@update:playing="handlePlayingUpdate2"
|
||||
@update:canplay="handleCanPlayMusic"
|
||||
>
|
||||
<template #default="{ }">
|
||||
<img v-if="!muted" style="width: 25px; height: 25px;" src="~/assets/image/music.svg" title="Click to mute" @click="toggleMute()">
|
||||
<img v-if="muted" style="width: 25px; height: 25px;" src="~/assets/image/music_muted.svg" title="Click to unmute" @click="toggleMute()">
|
||||
</template>
|
||||
</AudioElement>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import AudioElement from '../AudioElement.vue'
|
||||
import { useAudioStore } from '../../../stores/audio'
|
||||
|
||||
export default {
|
||||
name: 'MusicGainForest',
|
||||
components: { AudioElement },
|
||||
data () {
|
||||
return {
|
||||
audioContext: useAudioStore().getContext(),
|
||||
createdNodes: {} as any,
|
||||
musicReady: false,
|
||||
forest_src: window.location.origin + useRuntimeConfig().public.tracks.forest_src as string,
|
||||
fading: false,
|
||||
connected: false,
|
||||
muted: false
|
||||
}
|
||||
},
|
||||
beforeUnmount () {
|
||||
this.disconnectNodes()
|
||||
},
|
||||
methods: {
|
||||
toggleMute () {
|
||||
const element = this.$refs.Music as typeof AudioElement
|
||||
const audioElement = element.$refs.audioElement as HTMLMediaElement
|
||||
audioElement.muted = !audioElement.muted
|
||||
this.muted = audioElement.muted
|
||||
},
|
||||
disconnectNodes () {
|
||||
if (typeof this.createdNodes === 'object' && this.createdNodes !== null) {
|
||||
Object.values(this.createdNodes).forEach((node) => {
|
||||
// Check if the node exists and has a disconnect method
|
||||
if (node && typeof AudioNode) {
|
||||
const tobedisconnected = node as AudioNode
|
||||
tobedisconnected.disconnect()
|
||||
node = null
|
||||
}
|
||||
})
|
||||
this.createdNodes = null
|
||||
this.connected = false
|
||||
}
|
||||
},
|
||||
mute () {
|
||||
const element = this.$refs.Music as typeof AudioElement
|
||||
const audioElement = element.$refs.audioElement as HTMLMediaElement
|
||||
audioElement.muted = true
|
||||
this.muted = audioElement.muted
|
||||
},
|
||||
unmute () {
|
||||
const element = this.$refs.Music as typeof AudioElement
|
||||
const audioElement = element.$refs.audioElement as HTMLMediaElement
|
||||
audioElement.muted = false
|
||||
this.muted = audioElement.muted
|
||||
},
|
||||
// This method helps to get the ressources free when we stop playing the audio
|
||||
// without it would be louder each time we start playing
|
||||
refreshAudioContext () {
|
||||
const newAudioContext = new AudioContext()
|
||||
this.audioContext.close()
|
||||
useAudioStore().audioContext = newAudioContext
|
||||
this.audioContext = useAudioStore().getContext()
|
||||
},
|
||||
fadeInGains () {
|
||||
// useNuxtApp().$logger.log('Fade In Gains')
|
||||
|
||||
if (useAudioStore().playing !== true) { return }
|
||||
const fadeTime = this.audioContext.currentTime + 3.0
|
||||
this.fading = true
|
||||
this.unmute()
|
||||
const musicGain = this.createdNodes.musicGain
|
||||
this.createdNodes.musicGain.gain.setValueAtTime(0, this.audioContext.currentTime)
|
||||
musicGain.gain.linearRampToValueAtTime(1.0, fadeTime)
|
||||
|
||||
setTimeout(() => {
|
||||
this.fading = false
|
||||
}, fadeTime * 1000)
|
||||
},
|
||||
fadeOutGains () {
|
||||
if (this.createdNodes.musicGain) {
|
||||
const musicGainValue = this.createdNodes.musicGainValue.gain.value
|
||||
this.createdNodes.musicGain.gain.linearRampToValueAtTime(musicGainValue, this.audioContext.currentTime)
|
||||
this.createdNodes.musicGain.gain.linearRampToValueAtTime(0, this.audioContext.currentTime + 0.4)
|
||||
}
|
||||
},
|
||||
|
||||
handleCanPlayMusic () {
|
||||
// useNuxtApp().$logger.log('MusicElemeint has now playingstate: ' + state)
|
||||
this.musicReady = true
|
||||
this.handlePlayingUpdate(true)
|
||||
},
|
||||
|
||||
readyForWebaudio () {
|
||||
if (!this.musicReady) {
|
||||
// useNuxtApp().$logger.log('music not ready')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
handlePlayingUpdate2 (state: boolean) {
|
||||
// useNuxtApp().$logger.log('A new State reached us, it is a handlingPlay update' + state)
|
||||
// useNuxtApp().$logger.log('ReadyState of all:' + this.readyForWebaudio())
|
||||
if (!state) {
|
||||
this.mute()
|
||||
return
|
||||
}
|
||||
if (this.readyForWebaudio()) {
|
||||
if (state) {
|
||||
this.handlePlayingUpdate(state)
|
||||
} else {
|
||||
this.fadeOutGains()
|
||||
}
|
||||
} else if (this.readyForWebaudio()) {
|
||||
this.handlePlayingUpdate(state)
|
||||
}
|
||||
},
|
||||
handlePlayingUpdate (state: boolean) {
|
||||
if (state) {
|
||||
const musicElement = this.$refs.Music as typeof AudioElement
|
||||
const musicAudioElement = musicElement.$refs.audioElement as HTMLMediaElement
|
||||
const audioContext = this.audioContext
|
||||
const destination = this.audioContext.destination
|
||||
|
||||
this.createdNodes.musicGain ||= audioContext.createGain()
|
||||
|
||||
this.createdNodes.musicGain.gain.setValueAtTime(0, audioContext.currentTime)
|
||||
|
||||
if (musicAudioElement.currentSrc !== this.forest_src) {
|
||||
this.createdNodes.musicSource.disconnect()
|
||||
this.createdNodes.musicSource ||= audioContext.createMediaElementSource(musicAudioElement)
|
||||
this.createdNodes.musicSource.connect(this.createdNodes.musicGain)
|
||||
this.createdNodes.musicGain.connect(destination)
|
||||
}
|
||||
|
||||
this.createdNodes.musicSource ||= audioContext.createMediaElementSource(musicAudioElement)
|
||||
|
||||
// useNuxtApp().$logger.log({ currentlyCreatedNodes })
|
||||
this.createdNodes.musicSource.connect(this.createdNodes.musicGain)
|
||||
this.createdNodes.musicGain.connect(destination)
|
||||
this.createdNodes.musicGain.gain.cancelScheduledValues(this.audioContext.currentTime)
|
||||
this.createdNodes.musicGain.gain.setValueAtTime(0, this.audioContext.currentTime)
|
||||
|
||||
this.connected = true
|
||||
this.fadeInGains()
|
||||
useAudioStore().playing = true
|
||||
} else {
|
||||
// Music has just stopped react on it.
|
||||
// useNuxtApp().$logger.log('Stop everything webaudio is still running')
|
||||
this.fadeOutGains()
|
||||
this.createdNodes = []
|
||||
this.refreshAudioContext()
|
||||
this.connected = false
|
||||
}
|
||||
},
|
||||
updateMusicGain (volume: number) {
|
||||
if (this.createdNodes.musicGain) {
|
||||
this.createdNodes.musicGain.gain.linearRampToValueAtTime(volume, this.createdNodes.musicGain.context.currentTime + 0.30)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
170
components/experiments/homepages/MusicGainLagoon.vue
Normal file
@@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<div class="player">
|
||||
<AudioElement
|
||||
ref="Music"
|
||||
key="1"
|
||||
:src="lagoon_src"
|
||||
title="Lagoon"
|
||||
@update:volume="updateMusicGain"
|
||||
@update:playing="handlePlayingUpdate2"
|
||||
@update:canplay="handleCanPlayMusic"
|
||||
>
|
||||
<template #default="{ }">
|
||||
<img v-if="!muted" style="width: 25px; height: 25px;" src="~/assets/image/music.svg" title="Click to mute" @click="toggleMute()">
|
||||
<img v-if="muted" style="width: 25px; height: 25px;" src="~/assets/image/music_muted.svg" title="Click to unmute" @click="toggleMute()">
|
||||
</template>
|
||||
</AudioElement>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import AudioElement from '../AudioElement.vue'
|
||||
import { useAudioStore } from '../../../stores/audio'
|
||||
|
||||
export default {
|
||||
name: 'MusicGainLagoon',
|
||||
components: { AudioElement },
|
||||
data () {
|
||||
return {
|
||||
audioContext: useAudioStore().getContext(),
|
||||
createdNodes: {} as any,
|
||||
musicReady: false,
|
||||
|
||||
lagoon_src: window.location.origin + useRuntimeConfig().public.tracks.lagoon_src as string,
|
||||
fading: false,
|
||||
connected: false,
|
||||
muted: false
|
||||
}
|
||||
},
|
||||
beforeUnmount () {
|
||||
this.disconnectNodes()
|
||||
},
|
||||
methods: {
|
||||
disconnectNodes () {
|
||||
if (typeof this.createdNodes === 'object' && this.createdNodes !== null) {
|
||||
Object.values(this.createdNodes).forEach((node) => {
|
||||
// Check if the node exists and has a disconnect method
|
||||
if (node && typeof AudioNode) {
|
||||
const tobedisconnected = node as AudioNode
|
||||
tobedisconnected.disconnect()
|
||||
node = null
|
||||
}
|
||||
})
|
||||
this.createdNodes = null
|
||||
}
|
||||
},
|
||||
toggleMute () {
|
||||
const element = this.$refs.Music as typeof AudioElement
|
||||
const audioElement = element.$refs.audioElement as HTMLMediaElement
|
||||
audioElement.muted = !audioElement.muted
|
||||
this.muted = audioElement.muted
|
||||
},
|
||||
mute () {
|
||||
const element = this.$refs.Music as typeof AudioElement
|
||||
const audioElement = element.$refs.audioElement as HTMLMediaElement
|
||||
audioElement.muted = true
|
||||
this.muted = audioElement.muted
|
||||
},
|
||||
unmute () {
|
||||
const element = this.$refs.Music as typeof AudioElement
|
||||
const audioElement = element.$refs.audioElement as HTMLMediaElement
|
||||
audioElement.muted = false
|
||||
this.muted = audioElement.muted
|
||||
},
|
||||
// This method helps to get the ressources free when we stop playing the audio
|
||||
// without it would be louder each time we start playing
|
||||
refreshAudioContext () {
|
||||
const newAudioContext = new AudioContext()
|
||||
this.audioContext.close()
|
||||
useAudioStore().audioContext = newAudioContext
|
||||
this.audioContext = useAudioStore().getContext()
|
||||
},
|
||||
fadeInGains () {
|
||||
if (useAudioStore().playing !== true) { return }
|
||||
const fadeTime = this.audioContext.currentTime + 3.0
|
||||
this.fading = true
|
||||
this.unmute()
|
||||
const musicGain = this.createdNodes.musicGain
|
||||
this.createdNodes.musicGain.gain.setValueAtTime(0, this.audioContext.currentTime)
|
||||
musicGain.gain.linearRampToValueAtTime(1.0, fadeTime)
|
||||
|
||||
setTimeout(() => {
|
||||
this.fading = false
|
||||
}, fadeTime * 1000)
|
||||
},
|
||||
fadeOutGains () {
|
||||
if (this.createdNodes.musicGain) {
|
||||
const musicGainValue = this.createdNodes.musicGainValue.gain.value
|
||||
this.createdNodes.musicGain.gain.linearRampToValueAtTime(musicGainValue, this.audioContext.currentTime)
|
||||
this.createdNodes.musicGain.gain.linearRampToValueAtTime(0, this.audioContext.currentTime + 1.3)
|
||||
}
|
||||
},
|
||||
handleCanPlayMusic () {
|
||||
// useNuxtApp().$logger.log('MusicElemeint has now playingstate: ' + state)
|
||||
this.musicReady = true
|
||||
this.handlePlayingUpdate(true)
|
||||
},
|
||||
readyForWebaudio () {
|
||||
if (!this.musicReady) {
|
||||
// useNuxtApp().$logger.log('music not ready')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
handlePlayingUpdate2 (state: boolean) {
|
||||
if (!state) {
|
||||
this.mute()
|
||||
return
|
||||
}
|
||||
if (this.readyForWebaudio()) {
|
||||
if (state) {
|
||||
this.handlePlayingUpdate(state)
|
||||
} else {
|
||||
this.fadeOutGains()
|
||||
}
|
||||
} else if (this.readyForWebaudio()) {
|
||||
this.handlePlayingUpdate(state)
|
||||
}
|
||||
},
|
||||
handlePlayingUpdate (state: boolean) {
|
||||
if (state) {
|
||||
const musicElement = this.$refs.Music as typeof AudioElement
|
||||
const musicAudioElement = musicElement.$refs.audioElement as HTMLMediaElement
|
||||
const audioContext = this.audioContext
|
||||
const destination = this.audioContext.destination
|
||||
this.createdNodes.musicGain ||= audioContext.createGain()
|
||||
this.createdNodes.musicGain.gain.setValueAtTime(0, audioContext.currentTime)
|
||||
if (musicAudioElement.currentSrc !== this.lagoon_src) {
|
||||
this.createdNodes.musicSource.disconnect()
|
||||
this.createdNodes.musicSource = audioContext.createMediaElementSource(musicAudioElement)
|
||||
this.createdNodes.musicSource.connect(this.createdNodes.musicGain)
|
||||
this.createdNodes.musicGain.connect(destination)
|
||||
}
|
||||
|
||||
this.createdNodes.musicSource ||= audioContext.createMediaElementSource(musicAudioElement)
|
||||
|
||||
// useNuxtApp().$logger.log({ currentlyCreatedNodes })
|
||||
this.createdNodes.musicSource.connect(this.createdNodes.musicGain)
|
||||
this.createdNodes.musicGain.connect(destination)
|
||||
this.createdNodes.musicGain.gain.cancelScheduledValues(this.audioContext.currentTime)
|
||||
this.createdNodes.musicGain.gain.setValueAtTime(0, this.audioContext.currentTime)
|
||||
|
||||
this.connected = true
|
||||
this.fadeInGains()
|
||||
} else {
|
||||
// Music has just stopped react on it.
|
||||
// useNuxtApp().$logger.log('Stop everything webaudio is still running')
|
||||
this.fadeOutGains()
|
||||
this.createdNodes = []
|
||||
this.refreshAudioContext()
|
||||
this.connected = false
|
||||
}
|
||||
},
|
||||
updateMusicGain (volume: number) {
|
||||
if (this.createdNodes.musicGain) {
|
||||
this.createdNodes.musicGain.gain.linearRampToValueAtTime(volume, this.createdNodes.musicGain.context.currentTime + 0.30)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
179
components/experiments/homepages/MusicGainMeadow.vue
Normal file
@@ -0,0 +1,179 @@
|
||||
<template>
|
||||
<div class="player">
|
||||
<AudioElement
|
||||
ref="Music"
|
||||
key="1"
|
||||
:src="meadow_src"
|
||||
title="Meadow"
|
||||
@update:volume="updateMusicGain"
|
||||
@update:playing="handlePlayingUpdate2"
|
||||
@update:canplay="handleCanPlayMusic"
|
||||
>
|
||||
<template #default="{ }">
|
||||
<img v-if="!muted" style="width: 25px; height: 25px;" src="~/assets/image/music.svg" title="Click to mute" @click="toggleMute()">
|
||||
<img v-if="muted" style="width: 25px; height: 25px;" src="~/assets/image/music_muted.svg" title="Click to unmute" @click="toggleMute()">
|
||||
</template>
|
||||
</AudioElement>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import AudioElement from '../AudioElement.vue'
|
||||
import { useAudioStore } from '../../../stores/audio'
|
||||
|
||||
export default {
|
||||
name: 'MusicGainMeadow',
|
||||
components: { AudioElement },
|
||||
data () {
|
||||
return {
|
||||
audioContext: useAudioStore().getContext(),
|
||||
createdNodes: {} as any,
|
||||
musicReady: false,
|
||||
meadow_src: window.location.origin + useRuntimeConfig().public.tracks.meadow_src as string,
|
||||
fading: false,
|
||||
connected: false,
|
||||
muted: false
|
||||
}
|
||||
},
|
||||
beforeUnmount () {
|
||||
this.disconnectNodes()
|
||||
},
|
||||
methods: {
|
||||
disconnectNodes () {
|
||||
if (typeof this.createdNodes === 'object' && this.createdNodes !== null) {
|
||||
Object.values(this.createdNodes).forEach((node) => {
|
||||
// Check if the node exists and has a disconnect method
|
||||
if (node && typeof AudioNode) {
|
||||
const tobedisconnected = node as AudioNode
|
||||
tobedisconnected.disconnect()
|
||||
node = null
|
||||
}
|
||||
})
|
||||
this.createdNodes = null
|
||||
}
|
||||
},
|
||||
toggleMute () {
|
||||
const element = this.$refs.Music as typeof AudioElement
|
||||
const audioElement = element.$refs.audioElement as HTMLMediaElement
|
||||
audioElement.muted = !audioElement.muted
|
||||
this.muted = audioElement.muted
|
||||
},
|
||||
mute () {
|
||||
const element = this.$refs.Music as typeof AudioElement
|
||||
const audioElement = element.$refs.audioElement as HTMLMediaElement
|
||||
audioElement.muted = true
|
||||
this.muted = audioElement.muted
|
||||
},
|
||||
unmute () {
|
||||
const element = this.$refs.Music as typeof AudioElement
|
||||
const audioElement = element.$refs.audioElement as HTMLMediaElement
|
||||
audioElement.muted = false
|
||||
this.muted = audioElement.muted
|
||||
},
|
||||
// This method helps to get the ressources free when we stop playing the audio
|
||||
// without it would be louder each time we start playing
|
||||
refreshAudioContext () {
|
||||
const newAudioContext = new AudioContext()
|
||||
this.audioContext.close()
|
||||
useAudioStore().audioContext = newAudioContext
|
||||
this.audioContext = useAudioStore().getContext()
|
||||
},
|
||||
fadeInGains () {
|
||||
// useNuxtApp().$logger.log('Fade In Gains')
|
||||
|
||||
if (useAudioStore().playing !== true) { return }
|
||||
const fadeTime = this.audioContext.currentTime + 3.0
|
||||
this.fading = true
|
||||
this.unmute()
|
||||
const musicGain = this.createdNodes.musicGain
|
||||
this.createdNodes.musicGain.gain.setValueAtTime(0, this.audioContext.currentTime)
|
||||
musicGain.gain.linearRampToValueAtTime(1.0, fadeTime)
|
||||
|
||||
setTimeout(() => {
|
||||
this.fading = false
|
||||
}, fadeTime * 1000)
|
||||
},
|
||||
fadeOutGains () {
|
||||
if (this.createdNodes.musicGain) {
|
||||
const musicGainValue = this.createdNodes.musicGainValue.gain.value
|
||||
this.createdNodes.musicGain.gain.linearRampToValueAtTime(musicGainValue, this.audioContext.currentTime)
|
||||
this.createdNodes.musicGain.gain.linearRampToValueAtTime(0, this.audioContext.currentTime + 1.3)
|
||||
}
|
||||
},
|
||||
|
||||
handleCanPlayMusic () {
|
||||
// useNuxtApp().$logger.log('MusicElemeint has now playingstate: ' + state)
|
||||
this.musicReady = true
|
||||
this.handlePlayingUpdate(true)
|
||||
},
|
||||
|
||||
readyForWebaudio () {
|
||||
if (!this.musicReady) {
|
||||
// useNuxtApp().$logger.log('music not ready')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
handlePlayingUpdate2 (state: boolean) {
|
||||
// useNuxtApp().$logger.log('A new State reached us, it is a handlingPlay update' + state)
|
||||
// useNuxtApp().$logger.log('ReadyState of all:' + this.readyForWebaudio())
|
||||
if (!state) {
|
||||
this.mute()
|
||||
return
|
||||
}
|
||||
if (this.readyForWebaudio()) {
|
||||
if (state) {
|
||||
this.handlePlayingUpdate(state)
|
||||
} else {
|
||||
this.fadeOutGains()
|
||||
}
|
||||
} else if (this.readyForWebaudio()) {
|
||||
this.handlePlayingUpdate(state)
|
||||
}
|
||||
},
|
||||
handlePlayingUpdate (state: boolean) {
|
||||
if (state) {
|
||||
const musicElement = this.$refs.Music as typeof AudioElement
|
||||
const musicAudioElement = musicElement.$refs.audioElement as HTMLMediaElement
|
||||
const audioContext = this.audioContext
|
||||
const destination = this.audioContext.destination
|
||||
|
||||
this.createdNodes.musicGain ||= audioContext.createGain()
|
||||
|
||||
this.createdNodes.musicGain.gain.setValueAtTime(0, audioContext.currentTime)
|
||||
|
||||
if (musicAudioElement.currentSrc !== this.meadow_src) {
|
||||
this.createdNodes.musicSource.disconnect()
|
||||
this.createdNodes.musicSource = audioContext.createMediaElementSource(musicAudioElement)
|
||||
this.createdNodes.musicSource.connect(this.createdNodes.musicGain)
|
||||
this.createdNodes.musicGain.connect(destination)
|
||||
}
|
||||
|
||||
this.createdNodes.musicSource ||= audioContext.createMediaElementSource(musicAudioElement)
|
||||
|
||||
// useNuxtApp().$logger.log({ currentlyCreatedNodes })
|
||||
this.createdNodes.musicSource.connect(this.createdNodes.musicGain)
|
||||
this.createdNodes.musicGain.connect(destination)
|
||||
this.createdNodes.musicGain.gain.cancelScheduledValues(this.audioContext.currentTime)
|
||||
this.createdNodes.musicGain.gain.setValueAtTime(0, this.audioContext.currentTime)
|
||||
|
||||
this.connected = true
|
||||
this.fadeInGains()
|
||||
useAudioStore().playing = true
|
||||
} else {
|
||||
// Music has just stopped react on it.
|
||||
// useNuxtApp().$logger.log('Stop everything webaudio is still running')
|
||||
this.fadeOutGains()
|
||||
this.createdNodes = []
|
||||
this.refreshAudioContext()
|
||||
this.connected = false
|
||||
}
|
||||
},
|
||||
updateMusicGain (volume: number) {
|
||||
if (this.createdNodes.musicGain) {
|
||||
this.createdNodes.musicGain.gain.linearRampToValueAtTime(volume, this.createdNodes.musicGain.context.currentTime + 0.30)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
178
components/experiments/homepages/MusicGainTropic.vue
Normal file
@@ -0,0 +1,178 @@
|
||||
<template>
|
||||
<div class="player">
|
||||
<AudioElement
|
||||
ref="Music"
|
||||
key="1"
|
||||
:src="tropics_src"
|
||||
title="Tropic"
|
||||
@update:volume="updateMusicGain"
|
||||
@update:playing="handlePlayingUpdate2"
|
||||
@update:canplay="handleCanPlayMusic"
|
||||
>
|
||||
<template #default="{ }">
|
||||
<img v-if="!muted" style="width: 25px; height: 25px;" src="~/assets/image/music.svg" title="Click to mute" @click="toggleMute()">
|
||||
<img v-if="muted" style="width: 25px; height: 25px;" src="~/assets/image/music_muted.svg" title="Click to unmute" @click="toggleMute()">
|
||||
</template>
|
||||
</AudioElement>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import AudioElement from '../AudioElement.vue'
|
||||
import { useAudioStore } from '../../../stores/audio'
|
||||
|
||||
export default {
|
||||
name: 'MusicGainTropic',
|
||||
components: { AudioElement },
|
||||
data () {
|
||||
return {
|
||||
audioContext: useAudioStore().getContext(),
|
||||
createdNodes: {} as any,
|
||||
musicReady: false,
|
||||
|
||||
tropics_src: window.location.origin + useRuntimeConfig().public.tracks.tropics_src as string,
|
||||
fading: false,
|
||||
connected: false,
|
||||
muted: false
|
||||
}
|
||||
},
|
||||
beforeUnmount () {
|
||||
this.disconnectNodes()
|
||||
},
|
||||
methods: {
|
||||
disconnectNodes () {
|
||||
if (typeof this.createdNodes === 'object' && this.createdNodes !== null) {
|
||||
Object.values(this.createdNodes).forEach((node) => {
|
||||
// Check if the node exists and has a disconnect method
|
||||
if (node && typeof AudioNode) {
|
||||
const tobedisconnected = node as AudioNode
|
||||
tobedisconnected.disconnect()
|
||||
node = null
|
||||
}
|
||||
})
|
||||
}
|
||||
this.createdNodes = null
|
||||
},
|
||||
toggleMute () {
|
||||
const element = this.$refs.Music as typeof AudioElement
|
||||
const audioElement = element.$refs.audioElement as HTMLMediaElement
|
||||
audioElement.muted = !audioElement.muted
|
||||
this.muted = audioElement.muted
|
||||
},
|
||||
mute () {
|
||||
const element = this.$refs.Music as typeof AudioElement
|
||||
const audioElement = element.$refs.audioElement as HTMLMediaElement
|
||||
audioElement.muted = true
|
||||
this.muted = audioElement.muted
|
||||
},
|
||||
unmute () {
|
||||
const element = this.$refs.Music as typeof AudioElement
|
||||
const audioElement = element.$refs.audioElement as HTMLMediaElement
|
||||
audioElement.muted = false
|
||||
this.muted = audioElement.muted
|
||||
},
|
||||
// This method helps to get the ressources free when we stop playing the audio
|
||||
// without it would be louder each time we start playing
|
||||
refreshAudioContext () {
|
||||
const newAudioContext = new AudioContext()
|
||||
this.audioContext.close()
|
||||
useAudioStore().audioContext = newAudioContext
|
||||
this.audioContext = useAudioStore().getContext()
|
||||
},
|
||||
fadeInGains () {
|
||||
if (useAudioStore().playing !== true) { return }
|
||||
const fadeTime = this.audioContext.currentTime + 3.0
|
||||
this.fading = true
|
||||
this.unmute()
|
||||
const musicGain = this.createdNodes.musicGain
|
||||
this.createdNodes.musicGain.gain.setValueAtTime(0, this.audioContext.currentTime)
|
||||
musicGain.gain.linearRampToValueAtTime(1.0, fadeTime)
|
||||
|
||||
setTimeout(() => {
|
||||
this.fading = false
|
||||
}, fadeTime * 1000)
|
||||
},
|
||||
fadeOutGains () {
|
||||
if (this.createdNodes.musicGain) {
|
||||
const musicGainValue = this.createdNodes.musicGainValue.gain.value
|
||||
this.createdNodes.musicGain.gain.linearRampToValueAtTime(musicGainValue, this.audioContext.currentTime)
|
||||
this.createdNodes.musicGain.gain.linearRampToValueAtTime(0, this.audioContext.currentTime + 1.3)
|
||||
}
|
||||
},
|
||||
|
||||
handleCanPlayMusic () {
|
||||
// useNuxtApp().$logger.log('MusicElemeint has now playingstate: ' + state)
|
||||
this.musicReady = true
|
||||
this.handlePlayingUpdate(true)
|
||||
},
|
||||
|
||||
readyForWebaudio () {
|
||||
if (!this.musicReady) {
|
||||
// useNuxtApp().$logger.log('music not ready')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
handlePlayingUpdate2 (state: boolean) {
|
||||
// useNuxtApp().$logger.log('A new State reached us, it is a handlingPlay update' + state)
|
||||
// useNuxtApp().$logger.log('ReadyState of all:' + this.readyForWebaudio())
|
||||
if (!state) {
|
||||
this.mute()
|
||||
return
|
||||
}
|
||||
if (this.readyForWebaudio()) {
|
||||
if (state) {
|
||||
this.handlePlayingUpdate(state)
|
||||
} else {
|
||||
this.fadeOutGains()
|
||||
}
|
||||
} else if (this.readyForWebaudio()) {
|
||||
this.handlePlayingUpdate(state)
|
||||
}
|
||||
},
|
||||
handlePlayingUpdate (state: boolean) {
|
||||
if (state) {
|
||||
const musicElement = this.$refs.Music as typeof AudioElement
|
||||
const musicAudioElement = musicElement.$refs.audioElement as HTMLMediaElement
|
||||
const audioContext = this.audioContext
|
||||
const destination = this.audioContext.destination
|
||||
|
||||
this.createdNodes.musicGain ||= audioContext.createGain()
|
||||
|
||||
this.createdNodes.musicGain.gain.setValueAtTime(0, audioContext.currentTime)
|
||||
|
||||
if (musicAudioElement.currentSrc !== this.tropics_src) {
|
||||
this.createdNodes.musicSource.disconnect()
|
||||
this.createdNodes.musicSource = audioContext.createMediaElementSource(musicAudioElement)
|
||||
this.createdNodes.musicSource.connect(this.createdNodes.musicGain)
|
||||
this.createdNodes.musicGain.connect(destination)
|
||||
}
|
||||
|
||||
this.createdNodes.musicSource ||= audioContext.createMediaElementSource(musicAudioElement)
|
||||
|
||||
// useNuxtApp().$logger.log({ currentlyCreatedNodes })
|
||||
this.createdNodes.musicSource.connect(this.createdNodes.musicGain)
|
||||
this.createdNodes.musicGain.connect(destination)
|
||||
this.createdNodes.musicGain.gain.cancelScheduledValues(this.audioContext.currentTime)
|
||||
this.createdNodes.musicGain.gain.setValueAtTime(0, this.audioContext.currentTime)
|
||||
|
||||
this.connected = true
|
||||
this.fadeInGains()
|
||||
useAudioStore().playing = true
|
||||
} else {
|
||||
// Music has just stopped react on it.
|
||||
// useNuxtApp().$logger.log('Stop everything webaudio is still running')
|
||||
this.fadeOutGains()
|
||||
this.createdNodes = []
|
||||
this.refreshAudioContext()
|
||||
this.connected = false
|
||||
}
|
||||
},
|
||||
updateMusicGain (volume: number) {
|
||||
if (this.createdNodes.musicGain) {
|
||||
this.createdNodes.musicGain.gain.linearRampToValueAtTime(volume, this.createdNodes.musicGain.context.currentTime + 0.30)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
192
components/experiments/homepages/NoiseGain.vue
Normal file
@@ -0,0 +1,192 @@
|
||||
<template>
|
||||
<div class="player">
|
||||
<AudioElement
|
||||
ref="Noise"
|
||||
v-model:volume="volume"
|
||||
:src="tropics_src"
|
||||
@update:playing="handlePlayingUpdate2"
|
||||
@update:canplay="handleCanPlayNoise"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import AudioElement from '../AudioElement.vue'
|
||||
import { useAudioStore } from '../../../stores/audio'
|
||||
export default {
|
||||
name: 'NoiseGain',
|
||||
components: { AudioElement },
|
||||
data () {
|
||||
return {
|
||||
audioContext: useAudioStore().getContext(),
|
||||
createdNodes: {} as any,
|
||||
musicReady: false,
|
||||
tropics_src: window.location.origin + useRuntimeConfig().public.tracks.masking_src as string,
|
||||
fading: false,
|
||||
connected: false,
|
||||
muted: false,
|
||||
volume: useAudioStore().noiseVolume,
|
||||
previousVolume: useAudioStore().noiseVolume
|
||||
}
|
||||
},
|
||||
beforeUnmount () {
|
||||
this.disconnectNodes()
|
||||
},
|
||||
methods: {
|
||||
disconnectNodes () {
|
||||
if (typeof this.createdNodes === 'object' && this.createdNodes !== null) {
|
||||
Object.values(this.createdNodes).forEach((node) => {
|
||||
// Check if the node exists and has a disconnect method
|
||||
if (node && typeof AudioNode) {
|
||||
const tobedisconnected = node as AudioNode
|
||||
tobedisconnected.disconnect()
|
||||
node = null
|
||||
}
|
||||
})
|
||||
}
|
||||
this.createdNodes = null
|
||||
},
|
||||
toggleMute () {
|
||||
const element = this.$refs.Noise as typeof AudioElement
|
||||
const audioElement = element.$refs.audioElement as HTMLMediaElement
|
||||
if (this.muted) {
|
||||
// Unmute: Stelle den vorherigen Lautstärkewert wieder her
|
||||
this.muted = false
|
||||
audioElement.muted = false
|
||||
this.volume = this.previousVolume || 1 // Falls kein vorheriger Wert gespeichert ist, setze auf 1
|
||||
audioElement.volume = this.volume
|
||||
} else {
|
||||
// Mute: Speichere den aktuellen Lautstärkewert und mute das Audio
|
||||
this.previousVolume = this.volume
|
||||
this.volume = 0
|
||||
audioElement.volume = 0
|
||||
this.muted = true
|
||||
audioElement.muted = true
|
||||
}
|
||||
useAudioStore().setNoiseVolume(this.volume)
|
||||
element.$emit('update:volume', this.volume)
|
||||
},
|
||||
mute () {
|
||||
const element = this.$refs.Noise as typeof AudioElement
|
||||
const audioElement = element.$refs.audioElement as HTMLMediaElement
|
||||
audioElement.muted = true
|
||||
this.muted = audioElement.muted
|
||||
},
|
||||
unmute () {
|
||||
const element = this.$refs.Noise as typeof AudioElement
|
||||
const audioElement = element.$refs.audioElement as HTMLMediaElement
|
||||
audioElement.muted = false
|
||||
this.muted = audioElement.muted
|
||||
},
|
||||
// This method helps to get the ressources free when we stop playing the audio
|
||||
// without it would be louder each time we start playing
|
||||
refreshAudioContext () {
|
||||
const newAudioContext = new AudioContext()
|
||||
this.audioContext.close()
|
||||
useAudioStore().audioContext = newAudioContext
|
||||
this.audioContext = useAudioStore().getContext()
|
||||
},
|
||||
fadeInGains () {
|
||||
if (useAudioStore().playing !== true) { return }
|
||||
const fadeTime = this.audioContext.currentTime + 3.0
|
||||
this.fading = true
|
||||
this.unmute()
|
||||
const musicGain = this.createdNodes.musicGain
|
||||
this.createdNodes.musicGain.gain.setValueAtTime(0, this.audioContext.currentTime)
|
||||
musicGain.gain.linearRampToValueAtTime(1.0, fadeTime)
|
||||
|
||||
setTimeout(() => {
|
||||
this.fading = false
|
||||
}, fadeTime * 1000)
|
||||
},
|
||||
fadeOutGains () {
|
||||
if (this.createdNodes.musicGain) {
|
||||
const musicGainValue = this.createdNodes.musicGainValue.gain.value
|
||||
this.createdNodes.musicGain.gain.linearRampToValueAtTime(musicGainValue, this.audioContext.currentTime)
|
||||
this.createdNodes.musicGain.gain.linearRampToValueAtTime(0, this.audioContext.currentTime + 1.3)
|
||||
}
|
||||
},
|
||||
|
||||
handleCanPlayNoise () {
|
||||
// useNuxtApp().$logger.log('NoiseElemeint has now playingstate: ' + state)
|
||||
this.musicReady = true
|
||||
this.handlePlayingUpdate(true)
|
||||
},
|
||||
|
||||
readyForWebaudio () {
|
||||
if (!this.musicReady) {
|
||||
// useNuxtApp().$logger.log('music not ready')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
handlePlayingUpdate2 (state: boolean) {
|
||||
// useNuxtApp().$logger.log('A new State reached us, it is a handlingPlay update' + state)
|
||||
// useNuxtApp().$logger.log('ReadyState of all:' + this.readyForWebaudio())
|
||||
if (!state) {
|
||||
this.mute()
|
||||
return
|
||||
}
|
||||
if (this.readyForWebaudio()) {
|
||||
if (state) {
|
||||
this.handlePlayingUpdate(state)
|
||||
} else {
|
||||
this.fadeOutGains()
|
||||
}
|
||||
}
|
||||
},
|
||||
handlePlayingUpdate (state: boolean) {
|
||||
if (state) {
|
||||
const musicElement = this.$refs.Noise as typeof AudioElement
|
||||
const musicAudioElement = musicElement.$refs.audioElement as HTMLMediaElement
|
||||
const audioContext = this.audioContext
|
||||
const destination = this.audioContext.destination
|
||||
|
||||
this.createdNodes.musicGain ||= audioContext.createGain()
|
||||
|
||||
this.createdNodes.musicGain.gain.setValueAtTime(0, audioContext.currentTime)
|
||||
|
||||
if (musicAudioElement.currentSrc !== this.tropics_src) {
|
||||
this.createdNodes.musicSource.disconnect()
|
||||
this.createdNodes.musicSource = audioContext.createMediaElementSource(musicAudioElement)
|
||||
this.createdNodes.musicSource.connect(this.createdNodes.musicGain)
|
||||
this.createdNodes.musicGain.connect(destination)
|
||||
}
|
||||
|
||||
this.createdNodes.musicSource ||= audioContext.createMediaElementSource(musicAudioElement)
|
||||
|
||||
// useNuxtApp().$logger.log({ currentlyCreatedNodes })
|
||||
this.createdNodes.musicSource.connect(this.createdNodes.musicGain)
|
||||
this.createdNodes.musicGain.connect(destination)
|
||||
this.createdNodes.musicGain.gain.cancelScheduledValues(this.audioContext.currentTime)
|
||||
this.createdNodes.musicGain.gain.setValueAtTime(0, this.audioContext.currentTime)
|
||||
|
||||
this.connected = true
|
||||
this.fadeInGains()
|
||||
// useAudioStore().playing = true
|
||||
} else {
|
||||
// Noise has just stopped react on it.
|
||||
// useNuxtApp().$logger.log('Stop everything webaudio is still running')
|
||||
this.fadeOutGains()
|
||||
this.createdNodes = []
|
||||
this.refreshAudioContext()
|
||||
this.connected = false
|
||||
}
|
||||
},
|
||||
applyStoredVolume () {
|
||||
const element = this.$refs.Noise as typeof AudioElement
|
||||
const audioElement = element.$refs.audioElement as HTMLMediaElement
|
||||
|
||||
// Setze die Lautstärke des Audio-Elements
|
||||
audioElement.volume = this.volume
|
||||
|
||||
// Emitiere ein Event, um die Lautstärke in AudioElement zu aktualisieren
|
||||
element.$emit('update:volume', this.volume)
|
||||
},
|
||||
updateNoiseGain (volume: number) {
|
||||
if (this.createdNodes.musicGain) {
|
||||
this.createdNodes.musicGain.gain.linearRampToValueAtTime(volume, this.createdNodes.musicGain.context.currentTime + 0.30)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
239
components/experiments/homepages/NoiseMusicGainForest.vue
Normal file
@@ -0,0 +1,239 @@
|
||||
<template>
|
||||
<div class="player">
|
||||
<Microphone ref="Microphone" @update:attach="setupMicrophone" />
|
||||
<AudioElement
|
||||
ref="Noise"
|
||||
key="5"
|
||||
:src="noise_src"
|
||||
title="Noise"
|
||||
@update:volume="updateNoiseGain"
|
||||
@update:canplay="handleCanPlayNoise"
|
||||
>
|
||||
<template #default="{}">
|
||||
<img style="width: 25px" src="~/assets/image/noiseicon.svg">
|
||||
</template>
|
||||
</AudioElement>
|
||||
<AudioElement
|
||||
ref="Music"
|
||||
key="1"
|
||||
:src="forest_src"
|
||||
:playlist="music_src"
|
||||
title="Forest"
|
||||
@update:volume="updateMusicGain"
|
||||
@update:playing="handlePlayingUpdate2"
|
||||
@update:canplay="handleCanPlayMusic"
|
||||
>
|
||||
<template #default="{ }">
|
||||
<img style="width: 25px" src="~/assets/image/musicicon.svg">
|
||||
</template>
|
||||
</AudioElement>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import AudioElement from '../AudioElement.vue'
|
||||
import { useAudioStore } from '../../../stores/audio'
|
||||
import Microphone from '../tests/Microphone.vue'
|
||||
import { useDevicesStore } from '../../../stores/device'
|
||||
|
||||
export default {
|
||||
name: 'NoiseMusicGain',
|
||||
components: { AudioElement, Microphone },
|
||||
data () {
|
||||
return {
|
||||
audioContext: useAudioStore().getContext(),
|
||||
createdNodes: {} as any,
|
||||
noiseReady: false,
|
||||
musicReady: false,
|
||||
micReady: false,
|
||||
deviceReady: false,
|
||||
forest_src: window.location.origin + useRuntimeConfig().public.tracks.forest_src as string,
|
||||
music_src: [window.location.origin + useRuntimeConfig().public.tracks.lagoon_src as string, window.location.origin + useRuntimeConfig().public.tracks.tropics_src as string, window.location.origin + useRuntimeConfig().public.tracks.forest_src as string, window.location.origin + useRuntimeConfig().public.tracks.meadow_src as string] as string[],
|
||||
noise_src: window.location.origin + useRuntimeConfig().public.noise_src as string,
|
||||
fading: false,
|
||||
connected: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// This methodd gets a microphone stream from the micorphone component and creates the microphone node
|
||||
// need to be called before the noise device is connected to the audio graph
|
||||
setupMicrophone (stream:MediaStream) {
|
||||
try {
|
||||
this.createdNodes.microphone = this.audioContext.createMediaStreamSource(stream)
|
||||
this.micReady = true
|
||||
} catch (error: any) {
|
||||
this.micReady = false
|
||||
throw new Error(error.message)
|
||||
}
|
||||
},
|
||||
async setupDevice () {
|
||||
try {
|
||||
const deviceStore = useDevicesStore()
|
||||
await deviceStore.createFullBandDevice('adaptive_masking_controller_NoMusic')
|
||||
this.createdNodes.noiseDevice = deviceStore.getDeviceAudioNode('adaptive_masking_controller_NoMusic')
|
||||
this.deviceReady = true
|
||||
} catch (error) {
|
||||
this.deviceReady = false
|
||||
}
|
||||
},
|
||||
// This method helps to get the ressources free when we stop playing the audio
|
||||
// without it would be louder each time we start playing
|
||||
refreshAudioContext () {
|
||||
const newAudioContext = new AudioContext()
|
||||
this.audioContext.close()
|
||||
useAudioStore().audioContext = newAudioContext
|
||||
this.audioContext = useAudioStore().getContext()
|
||||
},
|
||||
fadeInGains () {
|
||||
// useNuxtApp().$logger.log('Fade In Gains')
|
||||
const fadeTime = this.audioContext.currentTime + 6.0
|
||||
this.fading = true
|
||||
const noiseGain = this.createdNodes.noiseGain
|
||||
const musicGain = this.createdNodes.musicGain
|
||||
noiseGain.gain.linearRampToValueAtTime(1.0, fadeTime)
|
||||
musicGain.gain.linearRampToValueAtTime(1.0, fadeTime)
|
||||
this.createdNodes.noiseSource.muted = false
|
||||
this.createdNodes.musicSource.muted = false
|
||||
setTimeout(() => {
|
||||
this.fading = false
|
||||
}, fadeTime * 1000)
|
||||
},
|
||||
fadeOutGains () {
|
||||
if (this.createdNodes.noiseGain) {
|
||||
const noiseGainValue = this.createdNodes.noiseGain.gain.value
|
||||
|
||||
this.createdNodes.noiseGain.gain.linearRampToValueAtTime(noiseGainValue, this.audioContext.currentTime)
|
||||
this.createdNodes.noiseGain.gain.linearRampToValueAtTime(0, this.audioContext.currentTime + 1.3)
|
||||
}
|
||||
if (this.createdNodes.musicGain) {
|
||||
const musicGainValue = this.createdNodes.noiseGain.gain.value
|
||||
this.createdNodes.noiseGain.gain.linearRampToValueAtTime(musicGainValue, this.audioContext.currentTime)
|
||||
this.createdNodes.musicGain.gain.linearRampToValueAtTime(0, this.audioContext.currentTime + 1.3)
|
||||
}
|
||||
},
|
||||
|
||||
handleCanPlayMusic () {
|
||||
// useNuxtApp().$logger.log('MusicElemeint has now playingstate: ' + state)
|
||||
this.musicReady = true
|
||||
},
|
||||
handleCanPlayNoise () {
|
||||
// useNuxtApp().$logger.log('NoiseElement has now playingstate: ' + state)
|
||||
this.noiseReady = true
|
||||
},
|
||||
readyForWebaudio () {
|
||||
if (!this.musicReady) {
|
||||
// useNuxtApp().$logger.log('music not ready')
|
||||
return false
|
||||
}
|
||||
if (!this.noiseReady) {
|
||||
// useNuxtApp().$logger.log('noise not ready')
|
||||
return false
|
||||
}
|
||||
if (!this.micReady) {
|
||||
// useNuxtApp().$logger.log('mic not ready')
|
||||
return false
|
||||
}
|
||||
if (!this.deviceReady) {
|
||||
// useNuxtApp().$logger.log('device not ready')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
async handlePlayingUpdate2 (state: boolean) {
|
||||
// useNuxtApp().$logger.log('A new State reached us, it is a handlingPlay update' + state)
|
||||
// useNuxtApp().$logger.log('ReadyState of all:' + this.readyForWebaudio())
|
||||
if (this.readyForWebaudio()) {
|
||||
if (state) {
|
||||
this.handlePlayingUpdate(state)
|
||||
this.fadeInGains()
|
||||
} else {
|
||||
this.fadeOutGains()
|
||||
}
|
||||
} else {
|
||||
if (!this.deviceReady) { await this.setupDevice() }
|
||||
if (!this.micReady) {
|
||||
// useNuxtApp().$logger.log('micophone not yet ready attach it!! ')
|
||||
// useNuxtApp().$logger.log('microphone attached' + stream)
|
||||
}
|
||||
if (this.readyForWebaudio()) {
|
||||
this.handlePlayingUpdate(state)
|
||||
} else {
|
||||
// useNuxtApp().$logger.log('Waiting for all devices to be ready')
|
||||
}
|
||||
}
|
||||
},
|
||||
handlePlayingUpdate (state: boolean) {
|
||||
// Stop the music again, mute it and set the noiseReady or musicReady to true
|
||||
if (state) {
|
||||
const noiseElement = this.$refs.Noise as typeof AudioElement
|
||||
const noiseAudioElement = noiseElement.$refs.audioElement as HTMLMediaElement
|
||||
const musicElement = this.$refs.Music as typeof AudioElement
|
||||
const musicAudioElement = musicElement.$refs.audioElement as HTMLMediaElement
|
||||
const audioContext = this.audioContext
|
||||
const destination = this.audioContext.destination
|
||||
|
||||
this.createdNodes.musicGain ||= audioContext.createGain()
|
||||
this.createdNodes.noiseGain ||= audioContext.createGain()
|
||||
this.createdNodes.musicGain.gain.setValueAtTime(0, audioContext.currentTime)
|
||||
this.createdNodes.noiseGain.gain.setValueAtTime(0, audioContext.currentTime)
|
||||
|
||||
if (musicAudioElement.currentSrc !== this.forest_src) {
|
||||
this.createdNodes.musicSource.disconnect()
|
||||
this.createdNodes.musicSource = audioContext.createMediaElementSource(musicAudioElement)
|
||||
this.createdNodes.musicSource.connect(this.createdNodes.musicGain)
|
||||
this.createdNodes.musicGain.connect(destination)
|
||||
}
|
||||
|
||||
this.createdNodes.noiseSource ||= audioContext.createMediaElementSource(noiseAudioElement)
|
||||
this.createdNodes.musicSource ||= audioContext.createMediaElementSource(musicAudioElement)
|
||||
|
||||
// HERE THE NOISE PATCH COMES INTO PLAY
|
||||
|
||||
this.createdNodes.micSplitter ||= audioContext.createChannelSplitter(2)
|
||||
this.createdNodes.noiseInputChannelSplitter ||= audioContext.createChannelSplitter(2)
|
||||
// useNuxtApp().$logger.log({ currentlyCreatedNodes })
|
||||
this.createdNodes.microphone.connect(this.createdNodes.micSplitter)
|
||||
this.createdNodes.noiseSource.connect(this.createdNodes.noiseInputChannelSplitter)
|
||||
this.createdNodes.micSplitter.connect(this.createdNodes.noiseDevice, 0, 0)
|
||||
this.createdNodes.noiseInputChannelSplitter.connect(this.createdNodes.noiseDevice, 0, 1)
|
||||
this.createdNodes.noiseInputChannelSplitter.connect(this.createdNodes.noiseDevice, 1, 2)
|
||||
|
||||
this.createdNodes.noiseDevice.connect(this.createdNodes.noiseGain)
|
||||
this.createdNodes.musicSource.connect(this.createdNodes.musicGain)
|
||||
|
||||
this.createdNodes.noiseGain.connect(destination)
|
||||
this.createdNodes.musicGain.connect(destination)
|
||||
|
||||
this.createdNodes.noiseGain.gain.cancelScheduledValues(this.audioContext.currentTime)
|
||||
this.createdNodes.musicGain.gain.cancelScheduledValues(this.audioContext.currentTime)
|
||||
|
||||
this.createdNodes.noiseGain.gain.setValueAtTime(0, this.audioContext.currentTime)
|
||||
this.createdNodes.musicGain.gain.setValueAtTime(0, this.audioContext.currentTime)
|
||||
|
||||
musicAudioElement.muted = false
|
||||
noiseAudioElement.muted = false
|
||||
this.connected = true
|
||||
this.fadeInGains()
|
||||
useAudioStore().playing = true
|
||||
} else {
|
||||
// Music has just stopped react on it.
|
||||
// useNuxtApp().$logger.log('Stop everything webaudio is still running')
|
||||
this.fadeOutGains()
|
||||
this.createdNodes = []
|
||||
this.refreshAudioContext()
|
||||
this.connected = false
|
||||
}
|
||||
},
|
||||
updateNoiseGain (volume: number) {
|
||||
if (this.createdNodes.noiseGain) {
|
||||
this.createdNodes.noiseGain.gain.linearRampToValueAtTime(volume, this.createdNodes.noiseGain.context.currentTime + 0.30)
|
||||
}
|
||||
},
|
||||
updateMusicGain (volume: number) {
|
||||
if (this.createdNodes.musicGain) {
|
||||
this.createdNodes.musicGain.gain.linearRampToValueAtTime(volume, this.createdNodes.musicGain.context.currentTime + 0.30)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
263
components/experiments/homepages/NoiseMusicGainLagoon.vue
Normal file
@@ -0,0 +1,263 @@
|
||||
<template>
|
||||
<div class="player">
|
||||
{{ controllValues }}
|
||||
<Microphone ref="Microphone" @update:attach="setupMicrophone" />
|
||||
<AudioElement
|
||||
ref="Noise"
|
||||
key="5"
|
||||
:src="noise_src"
|
||||
title="Noise"
|
||||
@update:volume="updateNoiseGain"
|
||||
@update:canplay="handleCanPlayNoise"
|
||||
>
|
||||
<template #default="{}">
|
||||
<img style="width: 25px" src="~/assets/image/noiseicon.svg">
|
||||
</template>
|
||||
</AudioElement>
|
||||
<AudioElement
|
||||
ref="Music"
|
||||
key="1"
|
||||
:src="lagoon_src"
|
||||
title="Lagoon"
|
||||
@update:volume="updateMusicGain"
|
||||
@update:playing="handlePlayingUpdate2"
|
||||
@update:canplay="handleCanPlayMusic"
|
||||
>
|
||||
<template #default="{ }">
|
||||
<img style="width: 25px" src="~/assets/image/musicicon.svg">
|
||||
</template>
|
||||
</AudioElement>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import AudioElement from '../AudioElement.vue'
|
||||
import { useAudioStore } from '../../../stores/audio'
|
||||
import Microphone from '../tests/Microphone.vue'
|
||||
import { useDevicesStore } from '../../../stores/device'
|
||||
|
||||
export default {
|
||||
name: 'NoiseMusicGain',
|
||||
components: { AudioElement, Microphone },
|
||||
data () {
|
||||
return {
|
||||
audioContext: useAudioStore().getContext(),
|
||||
createdNodes: {} as any,
|
||||
noiseReady: false,
|
||||
musicReady: false,
|
||||
micReady: false,
|
||||
deviceReady: false,
|
||||
lagoon_src: window.location.origin + useRuntimeConfig().public.tracks.lagoon_src as string,
|
||||
noise_src: window.location.origin + useRuntimeConfig().public.noise_src as string,
|
||||
fading: false,
|
||||
connected: false,
|
||||
lastUpdate: Date.now(),
|
||||
updateInterval: 125,
|
||||
controllValues: new Map() // milliseconds
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
// This methodd gets a microphone stream from the micorphone component and creates the microphone node
|
||||
// need to be called before the noise device is connected to the audio graph
|
||||
setupMicrophone (stream:MediaStream) {
|
||||
try {
|
||||
this.createdNodes.microphone = this.audioContext.createMediaStreamSource(stream)
|
||||
this.micReady = true
|
||||
} catch (error: any) {
|
||||
this.micReady = false
|
||||
throw new Error(error.message)
|
||||
}
|
||||
},
|
||||
async setupDevice () {
|
||||
try {
|
||||
const deviceStore = useDevicesStore()
|
||||
await deviceStore.createFullBandDevice('adaptive_masking_controller_NoMusic')
|
||||
this.createdNodes.noiseDevice = deviceStore.getDeviceAudioNode('adaptive_masking_controller_NoMusic')
|
||||
this.createdNodes.noiseDevice.port.onmessage = this.handleEvent
|
||||
this.deviceReady = true
|
||||
} catch (error) {
|
||||
this.deviceReady = false
|
||||
}
|
||||
},
|
||||
// This method helps to get the ressources free when we stop playing the audio
|
||||
// without it would be louder each time we start playing
|
||||
refreshAudioContext () {
|
||||
const newAudioContext = new AudioContext()
|
||||
this.audioContext.close()
|
||||
useAudioStore().audioContext = newAudioContext
|
||||
this.audioContext = useAudioStore().getContext()
|
||||
},
|
||||
|
||||
handleEvent (event:any) {
|
||||
const now = Date.now()
|
||||
if (now - this.lastUpdate < this.updateInterval) { return } // Skip this update
|
||||
|
||||
if (event.data && Array.isArray(event.data) && event.data.length > 1) {
|
||||
const eventDataDetail = event.data[1] // Assuming the relevant data is at index 1
|
||||
if (eventDataDetail && eventDataDetail.tag && eventDataDetail.payload && Array.isArray(eventDataDetail.payload)) {
|
||||
if (/out[3-9]|out1[01]/.test(eventDataDetail.tag)) {
|
||||
this.controllValues.set(eventDataDetail.tag, eventDataDetail.payload[0])
|
||||
this.lastUpdate = now
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
fadeInGains () {
|
||||
// useNuxtApp().$logger.log('Fade In Gains')
|
||||
const fadeTime = this.audioContext.currentTime + 6.0
|
||||
this.fading = true
|
||||
const noiseGain = this.createdNodes.noiseGain
|
||||
const musicGain = this.createdNodes.musicGain
|
||||
|
||||
noiseGain.gain.linearRampToValueAtTime(1.0, fadeTime)
|
||||
musicGain.gain.linearRampToValueAtTime(1.0, fadeTime)
|
||||
this.createdNodes.noiseSource.muted = false
|
||||
this.createdNodes.musicSource.muted = false
|
||||
setTimeout(() => {
|
||||
this.fading = false
|
||||
}, fadeTime * 1000)
|
||||
},
|
||||
fadeOutGains () {
|
||||
if (this.createdNodes.noiseGain) {
|
||||
const noiseGainValue = this.createdNodes.noiseGain.gain.value
|
||||
|
||||
this.createdNodes.noiseGain.gain.linearRampToValueAtTime(noiseGainValue, this.audioContext.currentTime)
|
||||
this.createdNodes.noiseGain.gain.linearRampToValueAtTime(0, this.audioContext.currentTime + 1.3)
|
||||
}
|
||||
if (this.createdNodes.musicGain) {
|
||||
const musicGainValue = this.createdNodes.noiseGain.gain.value
|
||||
this.createdNodes.noiseGain.gain.linearRampToValueAtTime(musicGainValue, this.audioContext.currentTime)
|
||||
this.createdNodes.musicGain.gain.linearRampToValueAtTime(0, this.audioContext.currentTime + 1.3)
|
||||
}
|
||||
},
|
||||
|
||||
handleCanPlayMusic (state: boolean) {
|
||||
// useNuxtApp().$logger.log('MusicElemeint has now playingstate: ' + state)
|
||||
this.musicReady = state
|
||||
},
|
||||
handleCanPlayNoise (state: boolean) {
|
||||
// useNuxtApp().$logger.log('NoiseElement has now playingstate: ' + state)
|
||||
this.noiseReady = state
|
||||
},
|
||||
readyForWebaudio () {
|
||||
if (!this.musicReady) {
|
||||
// useNuxtApp().$logger.log('music not ready')
|
||||
return false
|
||||
}
|
||||
if (!this.noiseReady) {
|
||||
// useNuxtApp().$logger.log('noise not ready')
|
||||
return false
|
||||
}
|
||||
if (!this.micReady) {
|
||||
// useNuxtApp().$logger.log('mic not ready')
|
||||
return false
|
||||
}
|
||||
if (!this.deviceReady) {
|
||||
// useNuxtApp().$logger.log('device not ready')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
async handlePlayingUpdate2 (state: boolean) {
|
||||
// useNuxtApp().$logger.log('A new State reached us, it is a handlingPlay update' + state)
|
||||
// useNuxtApp().$logger.log('ReadyState of all:' + this.readyForWebaudio())
|
||||
if (this.readyForWebaudio()) {
|
||||
this.handlePlayingUpdate(state)
|
||||
} else {
|
||||
if (!this.deviceReady) { await this.setupDevice() }
|
||||
if (!this.micReady) {
|
||||
// useNuxtApp().$logger.log('micophone not yet ready attach it!! ')
|
||||
// useNuxtApp().$logger.log('microphone attached' + stream)
|
||||
}
|
||||
if (this.readyForWebaudio()) {
|
||||
this.handlePlayingUpdate(state)
|
||||
} else {
|
||||
// useNuxtApp().$logger.log('Waiting for all devices to be ready')
|
||||
}
|
||||
}
|
||||
},
|
||||
handlePlayingUpdate (state: boolean) {
|
||||
// Stop the music again, mute it and set the noiseReady or musicReady to true
|
||||
if (state) {
|
||||
const noiseElement = this.$refs.Noise as typeof AudioElement
|
||||
const noiseAudioElement = noiseElement.$refs.audioElement as HTMLMediaElement
|
||||
const musicElement = this.$refs.Music as typeof AudioElement
|
||||
const musicAudioElement = musicElement.$refs.audioElement as HTMLMediaElement
|
||||
const audioContext = this.audioContext
|
||||
const destination = this.audioContext.destination
|
||||
|
||||
this.createdNodes.musicGain ||= audioContext.createGain()
|
||||
this.createdNodes.noiseGain ||= audioContext.createGain()
|
||||
this.createdNodes.musicGain.gain.setValueAtTime(0, audioContext.currentTime)
|
||||
this.createdNodes.noiseGain.gain.setValueAtTime(0, audioContext.currentTime)
|
||||
|
||||
this.createdNodes.noiseSource ||= audioContext.createMediaElementSource(noiseAudioElement)
|
||||
this.createdNodes.musicSource ||= audioContext.createMediaElementSource(musicAudioElement)
|
||||
|
||||
// HERE THE NOISE PATCH COMES INTO PLAY
|
||||
|
||||
this.createdNodes.micSplitter ||= audioContext.createChannelSplitter(2)
|
||||
this.createdNodes.noiseInputChannelSplitter ||= audioContext.createChannelSplitter(2)
|
||||
// useNuxtApp().$logger.log({ currentlyCreatedNodes })
|
||||
this.createdNodes.microphone.connect(this.createdNodes.micSplitter)
|
||||
this.createdNodes.noiseSource.connect(this.createdNodes.noiseInputChannelSplitter)
|
||||
this.createdNodes.micSplitter.connect(this.createdNodes.noiseDevice, 0, 0)
|
||||
this.createdNodes.noiseInputChannelSplitter.connect(this.createdNodes.noiseDevice, 0, 1)
|
||||
this.createdNodes.noiseInputChannelSplitter.connect(this.createdNodes.noiseDevice, 1, 2)
|
||||
|
||||
this.createdNodes.noiseDevice.connect(this.createdNodes.noiseGain)
|
||||
this.createdNodes.musicSource.connect(this.createdNodes.musicGain)
|
||||
|
||||
this.createdNodes.noiseGain.connect(destination)
|
||||
this.createdNodes.musicGain.connect(destination)
|
||||
|
||||
this.createdNodes.noiseGain.gain.cancelScheduledValues(this.audioContext.currentTime)
|
||||
this.createdNodes.musicGain.gain.cancelScheduledValues(this.audioContext.currentTime)
|
||||
|
||||
this.createdNodes.noiseGain.gain.setValueAtTime(0, this.audioContext.currentTime)
|
||||
this.createdNodes.musicGain.gain.setValueAtTime(0, this.audioContext.currentTime)
|
||||
|
||||
musicAudioElement.muted = false
|
||||
noiseAudioElement.muted = false
|
||||
this.connected = true
|
||||
this.fadeInGains()
|
||||
useAudioStore().playing = true
|
||||
} else {
|
||||
// Music has just stopped react on it.
|
||||
|
||||
this.fadeOutGains()
|
||||
this.createdNodes = []
|
||||
this.refreshAudioContext()
|
||||
this.connected = false
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.addMediaNavigationHandling()
|
||||
},
|
||||
addMediaNavigationHandling () {
|
||||
if ('mediaSession' in navigator) {
|
||||
// Play action
|
||||
navigator.mediaSession.setActionHandler('play', (_e) => {
|
||||
useAudioStore().setPlaying(true)
|
||||
})
|
||||
|
||||
// Pause action
|
||||
navigator.mediaSession.setActionHandler('pause', (_e) => {
|
||||
useAudioStore().setPlaying(false)
|
||||
})
|
||||
}
|
||||
},
|
||||
updateNoiseGain (volume: number) {
|
||||
if (this.createdNodes.noiseGain) {
|
||||
this.createdNodes.noiseGain.gain.linearRampToValueAtTime(volume, this.createdNodes.noiseGain.context.currentTime + 0.30)
|
||||
}
|
||||
},
|
||||
updateMusicGain (volume: number) {
|
||||
if (this.createdNodes.musicGain) {
|
||||
this.createdNodes.musicGain.gain.linearRampToValueAtTime(volume, this.createdNodes.musicGain.context.currentTime + 0.30)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
242
components/experiments/homepages/NoiseMusicGainMeadow.vue
Normal file
@@ -0,0 +1,242 @@
|
||||
<template>
|
||||
<div class="player">
|
||||
<Microphone ref="Microphone" @update:attach="setupMicrophone" />
|
||||
<AudioElement
|
||||
ref="Noise"
|
||||
key="5"
|
||||
:src="noise_src"
|
||||
title="Noise"
|
||||
@update:volume="updateNoiseGain"
|
||||
@update:canplay="handleCanPlayNoise"
|
||||
>
|
||||
<template #default="{}">
|
||||
<img style="width: 25px" src="~/assets/image/noiseicon.svg">
|
||||
</template>
|
||||
</AudioElement>
|
||||
<AudioElement
|
||||
ref="Music"
|
||||
key="1"
|
||||
:src="meadow_src"
|
||||
title="Meadow"
|
||||
@update:volume="updateMusicGain"
|
||||
@update:playing="handlePlayingUpdate2"
|
||||
@update:canplay="handleCanPlayMusic"
|
||||
>
|
||||
<template #default="{ }">
|
||||
<img style="width: 25px" src="~/assets/image/musicicon.svg">
|
||||
</template>
|
||||
</AudioElement>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import AudioElement from '../AudioElement.vue'
|
||||
import { useAudioStore } from '../../../stores/audio'
|
||||
import Microphone from '../tests/Microphone.vue'
|
||||
import { useDevicesStore } from '../../../stores/device'
|
||||
|
||||
export default {
|
||||
name: 'NoiseMusicGain',
|
||||
components: { AudioElement, Microphone },
|
||||
data () {
|
||||
return {
|
||||
audioContext: useAudioStore().getContext(),
|
||||
createdNodes: {} as any,
|
||||
noiseReady: false,
|
||||
musicReady: false,
|
||||
micReady: false,
|
||||
deviceReady: false,
|
||||
meadow_src: window.location.origin + useRuntimeConfig().public.tracks.meadow_src as string,
|
||||
noise_src: window.location.origin + useRuntimeConfig().public.noise_src as string,
|
||||
fading: false,
|
||||
connected: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
// This methodd gets a microphone stream from the micorphone component and creates the microphone node
|
||||
// need to be called before the noise device is connected to the audio graph
|
||||
setupMicrophone (stream:MediaStream) {
|
||||
try {
|
||||
this.createdNodes.microphone = this.audioContext.createMediaStreamSource(stream)
|
||||
this.micReady = true
|
||||
} catch (error: any) {
|
||||
this.micReady = false
|
||||
throw new Error(error.message)
|
||||
}
|
||||
},
|
||||
async setupDevice () {
|
||||
try {
|
||||
const deviceStore = useDevicesStore()
|
||||
await deviceStore.createFullBandDevice('adaptive_masking_controller_NoMusic')
|
||||
this.createdNodes.noiseDevice = deviceStore.getDeviceAudioNode('adaptive_masking_controller_NoMusic')
|
||||
this.deviceReady = true
|
||||
} catch (error) {
|
||||
this.deviceReady = false
|
||||
}
|
||||
},
|
||||
// This method helps to get the ressources free when we stop playing the audio
|
||||
// without it would be louder each time we start playing
|
||||
refreshAudioContext () {
|
||||
const newAudioContext = new AudioContext()
|
||||
this.audioContext.close()
|
||||
useAudioStore().audioContext = newAudioContext
|
||||
this.audioContext = useAudioStore().getContext()
|
||||
},
|
||||
fadeInGains () {
|
||||
// useNuxtApp().$logger.log('Fade In Gains')
|
||||
const fadeTime = this.audioContext.currentTime + 6.0
|
||||
this.fading = true
|
||||
const noiseGain = this.createdNodes.noiseGain
|
||||
const musicGain = this.createdNodes.musicGain
|
||||
|
||||
noiseGain.gain.linearRampToValueAtTime(1.0, fadeTime)
|
||||
musicGain.gain.linearRampToValueAtTime(1.0, fadeTime)
|
||||
this.createdNodes.noiseSource.muted = false
|
||||
this.createdNodes.musicSource.muted = false
|
||||
setTimeout(() => {
|
||||
this.fading = false
|
||||
}, fadeTime * 1000)
|
||||
},
|
||||
fadeOutGains () {
|
||||
if (this.createdNodes.noiseGain) {
|
||||
const noiseGainValue = this.createdNodes.noiseGain.gain.value
|
||||
|
||||
this.createdNodes.noiseGain.gain.linearRampToValueAtTime(noiseGainValue, this.audioContext.currentTime)
|
||||
this.createdNodes.noiseGain.gain.linearRampToValueAtTime(0, this.audioContext.currentTime + 1.3)
|
||||
}
|
||||
if (this.createdNodes.musicGain) {
|
||||
const musicGainValue = this.createdNodes.noiseGain.gain.value
|
||||
this.createdNodes.noiseGain.gain.linearRampToValueAtTime(musicGainValue, this.audioContext.currentTime)
|
||||
this.createdNodes.musicGain.gain.linearRampToValueAtTime(0, this.audioContext.currentTime + 1.3)
|
||||
}
|
||||
},
|
||||
|
||||
handleCanPlayMusic (state: boolean) {
|
||||
// useNuxtApp().$logger.log('MusicElemeint has now playingstate: ' + state)
|
||||
this.musicReady = state
|
||||
},
|
||||
handleCanPlayNoise (state: boolean) {
|
||||
// useNuxtApp().$logger.log('NoiseElement has now playingstate: ' + state)
|
||||
this.noiseReady = state
|
||||
},
|
||||
readyForWebaudio () {
|
||||
if (!this.musicReady) {
|
||||
// useNuxtApp().$logger.log('music not ready')
|
||||
return false
|
||||
}
|
||||
if (!this.noiseReady) {
|
||||
// useNuxtApp().$logger.log('noise not ready')
|
||||
return false
|
||||
}
|
||||
if (!this.micReady) {
|
||||
// useNuxtApp().$logger.log('mic not ready')
|
||||
return false
|
||||
}
|
||||
if (!this.deviceReady) {
|
||||
// useNuxtApp().$logger.log('device not ready')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
async handlePlayingUpdate2 (state: boolean) {
|
||||
// useNuxtApp().$logger.log('A new State reached us, it is a handlingPlay update' + state)
|
||||
// useNuxtApp().$logger.log('ReadyState of all:' + this.readyForWebaudio())
|
||||
if (this.readyForWebaudio()) {
|
||||
this.handlePlayingUpdate(state)
|
||||
} else {
|
||||
if (!this.deviceReady) { await this.setupDevice() }
|
||||
if (!this.micReady) {
|
||||
// useNuxtApp().$logger.log('micophone not yet ready attach it!! ')
|
||||
// useNuxtApp().$logger.log('microphone attached' + stream)
|
||||
}
|
||||
if (this.readyForWebaudio()) {
|
||||
this.handlePlayingUpdate(state)
|
||||
} else {
|
||||
// useNuxtApp().$logger.log('Waiting for all devices to be ready')
|
||||
}
|
||||
}
|
||||
},
|
||||
handlePlayingUpdate (state: boolean) {
|
||||
// Stop the music again, mute it and set the noiseReady or musicReady to true
|
||||
if (state) {
|
||||
const noiseElement = this.$refs.Noise as typeof AudioElement
|
||||
const noiseAudioElement = noiseElement.$refs.audioElement as HTMLMediaElement
|
||||
const musicElement = this.$refs.Music as typeof AudioElement
|
||||
const musicAudioElement = musicElement.$refs.audioElement as HTMLMediaElement
|
||||
const audioContext = this.audioContext
|
||||
const destination = this.audioContext.destination
|
||||
|
||||
this.createdNodes.musicGain ||= audioContext.createGain()
|
||||
this.createdNodes.noiseGain ||= audioContext.createGain()
|
||||
this.createdNodes.musicGain.gain.setValueAtTime(0, audioContext.currentTime)
|
||||
this.createdNodes.noiseGain.gain.setValueAtTime(0, audioContext.currentTime)
|
||||
|
||||
this.createdNodes.noiseSource ||= audioContext.createMediaElementSource(noiseAudioElement)
|
||||
this.createdNodes.musicSource ||= audioContext.createMediaElementSource(musicAudioElement)
|
||||
|
||||
// HERE THE NOISE PATCH COMES INTO PLAY
|
||||
|
||||
this.createdNodes.micSplitter ||= audioContext.createChannelSplitter(2)
|
||||
this.createdNodes.noiseInputChannelSplitter ||= audioContext.createChannelSplitter(2)
|
||||
this.createdNodes.microphone.connect(this.createdNodes.micSplitter)
|
||||
this.createdNodes.noiseSource.connect(this.createdNodes.noiseInputChannelSplitter)
|
||||
this.createdNodes.micSplitter.connect(this.createdNodes.noiseDevice, 0, 0)
|
||||
this.createdNodes.noiseInputChannelSplitter.connect(this.createdNodes.noiseDevice, 0, 1)
|
||||
this.createdNodes.noiseInputChannelSplitter.connect(this.createdNodes.noiseDevice, 1, 2)
|
||||
|
||||
this.createdNodes.noiseDevice.connect(this.createdNodes.noiseGain)
|
||||
this.createdNodes.musicSource.connect(this.createdNodes.musicGain)
|
||||
|
||||
this.createdNodes.noiseGain.connect(destination)
|
||||
this.createdNodes.musicGain.connect(destination)
|
||||
|
||||
this.createdNodes.noiseGain.gain.cancelScheduledValues(this.audioContext.currentTime)
|
||||
this.createdNodes.musicGain.gain.cancelScheduledValues(this.audioContext.currentTime)
|
||||
|
||||
this.createdNodes.noiseGain.gain.setValueAtTime(0, this.audioContext.currentTime)
|
||||
this.createdNodes.musicGain.gain.setValueAtTime(0, this.audioContext.currentTime)
|
||||
|
||||
musicAudioElement.muted = false
|
||||
noiseAudioElement.muted = false
|
||||
this.connected = true
|
||||
this.fadeInGains()
|
||||
useAudioStore().playing = true
|
||||
} else {
|
||||
// Music has just stopped react on it.
|
||||
// useNuxtApp().$logger.log('Stop everything webaudio is still running')
|
||||
this.fadeOutGains()
|
||||
this.createdNodes = []
|
||||
this.refreshAudioContext()
|
||||
this.connected = false
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.addMediaNavigationHandling()
|
||||
},
|
||||
addMediaNavigationHandling () {
|
||||
if ('mediaSession' in navigator) {
|
||||
// Play action
|
||||
navigator.mediaSession.setActionHandler('play', (_e) => {
|
||||
useAudioStore().setPlaying(true)
|
||||
})
|
||||
|
||||
// Pause action
|
||||
navigator.mediaSession.setActionHandler('pause', (_e) => {
|
||||
useAudioStore().setPlaying(false)
|
||||
})
|
||||
}
|
||||
},
|
||||
updateNoiseGain (volume: number) {
|
||||
if (this.createdNodes.noiseGain) {
|
||||
this.createdNodes.noiseGain.gain.linearRampToValueAtTime(volume, this.createdNodes.noiseGain.context.currentTime + 0.30)
|
||||
}
|
||||
},
|
||||
updateMusicGain (volume: number) {
|
||||
if (this.createdNodes.musicGain) {
|
||||
this.createdNodes.musicGain.gain.linearRampToValueAtTime(volume, this.createdNodes.musicGain.context.currentTime + 0.30)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
241
components/experiments/homepages/NoiseMusicGainTropics.vue
Normal file
@@ -0,0 +1,241 @@
|
||||
<template>
|
||||
<div class="player">
|
||||
<Microphone ref="Microphone" @update:attach="setupMicrophone" />
|
||||
<AudioElement
|
||||
ref="Noise"
|
||||
key="5"
|
||||
:src="noise_src"
|
||||
title="Noise"
|
||||
@update:volume="updateNoiseGain"
|
||||
@update:canplay="handleCanPlayNoise"
|
||||
>
|
||||
<template #default="{}">
|
||||
<img style="width: 25px" src="~/assets/image/noiseicon.svg">
|
||||
</template>
|
||||
</AudioElement>
|
||||
<AudioElement
|
||||
ref="Music"
|
||||
key="1"
|
||||
:src="tropics_src"
|
||||
title="Tropics"
|
||||
@update:volume="updateMusicGain"
|
||||
@update:playing="handlePlayingUpdate2"
|
||||
@update:canplay="handleCanPlayMusic"
|
||||
>
|
||||
<template #default="{ }">
|
||||
<img style="width: 25px" src="~/assets/image/musicicon.svg">
|
||||
</template>
|
||||
</AudioElement>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import AudioElement from '../AudioElement.vue'
|
||||
import { useAudioStore } from '../../../stores/audio'
|
||||
import Microphone from '../tests/Microphone.vue'
|
||||
import { useDevicesStore } from '../../../stores/device'
|
||||
|
||||
export default {
|
||||
name: 'NoiseMusicGain',
|
||||
components: { AudioElement, Microphone },
|
||||
data () {
|
||||
return {
|
||||
audioContext: useAudioStore().getContext(),
|
||||
createdNodes: {} as any,
|
||||
noiseReady: false,
|
||||
musicReady: false,
|
||||
micReady: false,
|
||||
deviceReady: false,
|
||||
tropics_src: window.location.origin + useRuntimeConfig().public.tracks.tropics_src as string,
|
||||
noise_src: window.location.origin + useRuntimeConfig().public.noise_src as string,
|
||||
fading: false,
|
||||
connected: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
// This methodd gets a microphone stream from the micorphone component and creates the microphone node
|
||||
// need to be called before the noise device is connected to the audio graph
|
||||
setupMicrophone (stream:MediaStream) {
|
||||
try {
|
||||
this.createdNodes.microphone = this.audioContext.createMediaStreamSource(stream)
|
||||
this.micReady = true
|
||||
} catch (error: any) {
|
||||
this.micReady = false
|
||||
throw new Error(error.message)
|
||||
}
|
||||
},
|
||||
async setupDevice () {
|
||||
try {
|
||||
const deviceStore = useDevicesStore()
|
||||
await deviceStore.createFullBandDevice('adaptive_masking_controller_NoMusic')
|
||||
this.createdNodes.noiseDevice = deviceStore.getDeviceAudioNode('adaptive_masking_controller_NoMusic')
|
||||
this.deviceReady = true
|
||||
} catch (error) {
|
||||
this.deviceReady = false
|
||||
}
|
||||
},
|
||||
// This method helps to get the ressources free when we stop playing the audio
|
||||
// without it would be louder each time we start playing
|
||||
refreshAudioContext () {
|
||||
const newAudioContext = new AudioContext()
|
||||
this.audioContext.close()
|
||||
useAudioStore().audioContext = newAudioContext
|
||||
this.audioContext = useAudioStore().getContext()
|
||||
},
|
||||
fadeInGains () {
|
||||
// useNuxtApp().$logger.log('Fade In Gains')
|
||||
const fadeTime = this.audioContext.currentTime + 6.0
|
||||
this.fading = true
|
||||
const noiseGain = this.createdNodes.noiseGain
|
||||
const musicGain = this.createdNodes.musicGain
|
||||
|
||||
noiseGain.gain.linearRampToValueAtTime(1.0, fadeTime)
|
||||
musicGain.gain.linearRampToValueAtTime(1.0, fadeTime)
|
||||
this.createdNodes.noiseSource.muted = false
|
||||
this.createdNodes.musicSource.muted = false
|
||||
setTimeout(() => {
|
||||
this.fading = false
|
||||
}, fadeTime * 1000)
|
||||
},
|
||||
fadeOutGains () {
|
||||
if (this.createdNodes.noiseGain) {
|
||||
const noiseGainValue = this.createdNodes.noiseGain.gain.value
|
||||
|
||||
this.createdNodes.noiseGain.gain.linearRampToValueAtTime(noiseGainValue, this.audioContext.currentTime)
|
||||
this.createdNodes.noiseGain.gain.linearRampToValueAtTime(0, this.audioContext.currentTime + 1.3)
|
||||
}
|
||||
if (this.createdNodes.musicGain) {
|
||||
const musicGainValue = this.createdNodes.noiseGain.gain.value
|
||||
this.createdNodes.noiseGain.gain.linearRampToValueAtTime(musicGainValue, this.audioContext.currentTime)
|
||||
this.createdNodes.musicGain.gain.linearRampToValueAtTime(0, this.audioContext.currentTime + 1.3)
|
||||
}
|
||||
},
|
||||
|
||||
handleCanPlayMusic (state: boolean) {
|
||||
// useNuxtApp().$logger.log('MusicElemeint has now playingstate: ' + state)
|
||||
this.musicReady = state
|
||||
},
|
||||
handleCanPlayNoise (state: boolean) {
|
||||
// useNuxtApp().$logger.log('NoiseElement has now playingstate: ' + state)
|
||||
this.noiseReady = state
|
||||
},
|
||||
readyForWebaudio () {
|
||||
if (!this.musicReady) {
|
||||
// useNuxtApp().$logger.log('music not ready')
|
||||
return false
|
||||
}
|
||||
if (!this.noiseReady) {
|
||||
// useNuxtApp().$logger.log('noise not ready')
|
||||
return false
|
||||
}
|
||||
if (!this.micReady) {
|
||||
// useNuxtApp().$logger.log('mic not ready')
|
||||
return false
|
||||
}
|
||||
if (!this.deviceReady) {
|
||||
// useNuxtApp().$logger.log('device not ready')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
async handlePlayingUpdate2 (state: boolean) {
|
||||
// useNuxtApp().$logger.log('A new State reached us, it is a handlingPlay update' + state)
|
||||
// useNuxtApp().$logger.log('ReadyState of all:' + this.readyForWebaudio())
|
||||
if (this.readyForWebaudio()) {
|
||||
this.handlePlayingUpdate(state)
|
||||
} else {
|
||||
if (!this.deviceReady) { await this.setupDevice() }
|
||||
if (!this.micReady) {
|
||||
// useNuxtApp().$logger.log('micophone not yet ready attach it!! ')
|
||||
}
|
||||
if (this.readyForWebaudio()) {
|
||||
this.handlePlayingUpdate(state)
|
||||
} else {
|
||||
// useNuxtApp().$logger.log('Waiting for all devices to be ready')
|
||||
}
|
||||
}
|
||||
},
|
||||
handlePlayingUpdate (state: boolean) {
|
||||
// Stop the music again, mute it and set the noiseReady or musicReady to true
|
||||
if (state) {
|
||||
const noiseElement = this.$refs.Noise as typeof AudioElement
|
||||
const noiseAudioElement = noiseElement.$refs.audioElement as HTMLMediaElement
|
||||
const musicElement = this.$refs.Music as typeof AudioElement
|
||||
const musicAudioElement = musicElement.$refs.audioElement as HTMLMediaElement
|
||||
const audioContext = this.audioContext
|
||||
const destination = this.audioContext.destination
|
||||
|
||||
this.createdNodes.musicGain ||= audioContext.createGain()
|
||||
this.createdNodes.noiseGain ||= audioContext.createGain()
|
||||
this.createdNodes.musicGain.gain.setValueAtTime(0, audioContext.currentTime)
|
||||
this.createdNodes.noiseGain.gain.setValueAtTime(0, audioContext.currentTime)
|
||||
|
||||
this.createdNodes.noiseSource = audioContext.createMediaElementSource(noiseAudioElement)
|
||||
this.createdNodes.musicSource = audioContext.createMediaElementSource(musicAudioElement)
|
||||
|
||||
// HERE THE NOISE PATCH COMES INTO PLAY
|
||||
|
||||
this.createdNodes.micSplitter ||= audioContext.createChannelSplitter(2)
|
||||
this.createdNodes.noiseInputChannelSplitter ||= audioContext.createChannelSplitter(2)
|
||||
this.createdNodes.microphone.connect(this.createdNodes.micSplitter)
|
||||
this.createdNodes.noiseSource.connect(this.createdNodes.noiseInputChannelSplitter)
|
||||
this.createdNodes.micSplitter.connect(this.createdNodes.noiseDevice, 0, 0)
|
||||
this.createdNodes.noiseInputChannelSplitter.connect(this.createdNodes.noiseDevice, 0, 1)
|
||||
this.createdNodes.noiseInputChannelSplitter.connect(this.createdNodes.noiseDevice, 1, 2)
|
||||
|
||||
this.createdNodes.noiseDevice.connect(this.createdNodes.noiseGain)
|
||||
this.createdNodes.musicSource.connect(this.createdNodes.musicGain)
|
||||
|
||||
this.createdNodes.noiseGain.connect(destination)
|
||||
this.createdNodes.musicGain.connect(destination)
|
||||
|
||||
this.createdNodes.noiseGain.gain.cancelScheduledValues(this.audioContext.currentTime)
|
||||
this.createdNodes.musicGain.gain.cancelScheduledValues(this.audioContext.currentTime)
|
||||
|
||||
this.createdNodes.noiseGain.gain.setValueAtTime(0, this.audioContext.currentTime)
|
||||
this.createdNodes.musicGain.gain.setValueAtTime(0, this.audioContext.currentTime)
|
||||
|
||||
musicAudioElement.muted = false
|
||||
noiseAudioElement.muted = false
|
||||
this.connected = true
|
||||
this.fadeInGains()
|
||||
useAudioStore().playing = true
|
||||
} else {
|
||||
// Music has just stopped react on it.
|
||||
// useNuxtApp().$logger.log('Stop everything webaudio is still running')
|
||||
this.fadeOutGains()
|
||||
this.createdNodes = []
|
||||
this.refreshAudioContext()
|
||||
this.connected = false
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.addMediaNavigationHandling()
|
||||
},
|
||||
addMediaNavigationHandling () {
|
||||
if ('mediaSession' in navigator) {
|
||||
// Play action
|
||||
navigator.mediaSession.setActionHandler('play', (_e) => {
|
||||
useAudioStore().setPlaying(true)
|
||||
})
|
||||
|
||||
// Pause action
|
||||
navigator.mediaSession.setActionHandler('pause', (_e) => {
|
||||
useAudioStore().setPlaying(false)
|
||||
})
|
||||
}
|
||||
},
|
||||
updateNoiseGain (volume: number) {
|
||||
if (this.createdNodes.noiseGain) {
|
||||
this.createdNodes.noiseGain.gain.linearRampToValueAtTime(volume, this.createdNodes.noiseGain.context.currentTime + 0.30)
|
||||
}
|
||||
},
|
||||
updateMusicGain (volume: number) {
|
||||
if (this.createdNodes.musicGain) {
|
||||
this.createdNodes.musicGain.gain.linearRampToValueAtTime(volume, this.createdNodes.musicGain.context.currentTime + 0.30)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
266
components/experiments/homepages/RNBODevice.vue
Normal file
@@ -0,0 +1,266 @@
|
||||
<template>
|
||||
<div class="player">
|
||||
<div v-if="deviceReady">RNBOValues: {{ createdNodes.noiseDevice }}</div>
|
||||
<Microphone ref="Microphone" @update:attach="setupMicrophone" />
|
||||
<AudioElement
|
||||
ref="Noise"
|
||||
key="5"
|
||||
:src="noise_src"
|
||||
title="Noise"
|
||||
@update:volume="updateNoiseGain"
|
||||
@update:canplay="handleCanPlayNoise"
|
||||
@update:playing="handlePlayingUpdate2"
|
||||
>
|
||||
<template #default="{}">
|
||||
<img v-if="!muted" style="width: 25px; height: 25px;" src="~/assets/image/sound.svg" title="Click to mute" @click="toggleMute()">
|
||||
<img v-if="muted" style="width: 25px; height: 25px;" src="~/assets/image/sound_muted.svg" title="Click to unmute" @click="toggleMute()">
|
||||
</template>
|
||||
</AudioElement>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import type { Device } from '@rnbo/js'
|
||||
import AudioElement from '../AudioElement.vue'
|
||||
import { useAudioStore } from '../../../stores/audio'
|
||||
import Microphone from '../tests/Microphone.vue'
|
||||
import { useDevicesStore } from '../../../stores/device'
|
||||
|
||||
export default {
|
||||
name: 'RNBODevice',
|
||||
components: { AudioElement, Microphone },
|
||||
emits: { 'update:control-value': null },
|
||||
data () {
|
||||
return {
|
||||
audioContext: useAudioStore().getContext(),
|
||||
createdNodes: {} as any,
|
||||
noiseReady: false,
|
||||
micReady: false,
|
||||
deviceReady: false,
|
||||
noise_src: window.location.origin + useRuntimeConfig().public.noise_src as string,
|
||||
fading: false,
|
||||
connected: false,
|
||||
muted: false
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
toggleMute () {
|
||||
const noiseElement = this.$refs.Noise as typeof AudioElement
|
||||
const noiseAudioElement = noiseElement.$refs.audioElement as HTMLMediaElement
|
||||
noiseAudioElement.muted = !noiseAudioElement.muted
|
||||
this.muted = noiseAudioElement.muted
|
||||
},
|
||||
mute () {
|
||||
const noiseElement = this.$refs.Noise as typeof AudioElement
|
||||
const noiseAudioElement = noiseElement.$refs.audioElement as HTMLMediaElement
|
||||
noiseAudioElement.muted = true
|
||||
this.muted = true
|
||||
},
|
||||
unmute () {
|
||||
const noiseElement = this.$refs.Noise as typeof AudioElement
|
||||
const noiseAudioElement = noiseElement.$refs.audioElement as HTMLMediaElement
|
||||
noiseAudioElement.muted = false
|
||||
this.muted = false
|
||||
},
|
||||
// This methodd gets a microphone stream from the micorphone component and creates the microphone node
|
||||
// need to be called before the noise device is connected to the audio graph
|
||||
setupMicrophone (stream:MediaStream) {
|
||||
useNuxtApp().$logger.log('setup Microphone')
|
||||
try {
|
||||
this.createdNodes.microphone ||= this.audioContext.createMediaStreamSource(stream)
|
||||
this.micReady = true
|
||||
} catch (error: any) {
|
||||
this.micReady = false
|
||||
throw new Error(error.message)
|
||||
}
|
||||
},
|
||||
// This method setup a RNBO Device, it gets the name of the Patch and add the noise audio node to createdNodes
|
||||
async setupDevice () {
|
||||
await useAudioStore().ensureAudioContextRunning()
|
||||
useNuxtApp().$logger.log('setup Device')
|
||||
try {
|
||||
const deviceStore = useDevicesStore()
|
||||
const device = await deviceStore.createNoiseDevice('adaptive_masking_controller_NoMusic') as Device // ich bekomme keine analyse Werte raus
|
||||
this.createdNodes.noiseDevice = deviceStore.getDeviceAudioNode('adaptive_masking_controller_NoMusic')
|
||||
this.deviceReady = true
|
||||
this.attachDBValueListener(device)
|
||||
} catch (error) {
|
||||
useNuxtApp().$logger.error('Error setting up device, fall back.', { error })
|
||||
this.deviceReady = false
|
||||
}
|
||||
},
|
||||
|
||||
// This method takes the controll value before ramp and controls the volume of music
|
||||
attachDBValueListener (noiseDevice: Device) {
|
||||
noiseDevice.messageEvent.subscribe((ev: any) => {
|
||||
try {
|
||||
// if (ev.tag === 'out4') { // out4 represents controll value before Timeramp
|
||||
if (ev.tag === 'out3') { // out3 represents controll value in dB after Timeramp
|
||||
const newValue = ev.payload
|
||||
useNuxtApp().$logger.log('out3= ' + newValue[0])
|
||||
}
|
||||
if (ev.tag === 'out4') { // out3 represents controll value in dB after Timeramp
|
||||
const newValue = ev.payload
|
||||
useNuxtApp().$logger.log('out4= ' + newValue[0])
|
||||
}
|
||||
if (ev.tag === 'out5') { // out3 represents controll value in dB after Timeramp
|
||||
const newValue = ev.payload
|
||||
useNuxtApp().$logger.log('out5= ' + newValue[0])
|
||||
}
|
||||
if (ev.tag === 'ou6') { // out3 represents controll value in dB after Timeramp
|
||||
const newValue = ev.payload
|
||||
useNuxtApp().$logger.log('out6= ' + newValue[0])
|
||||
}
|
||||
if (ev.tag === 'out7') { // out3 represents controll value in dB after Timeramp
|
||||
const newValue = ev.payload
|
||||
useNuxtApp().$logger.log('out7= ' + newValue[0])
|
||||
}
|
||||
if (ev.tag === 'out8') { // out3 represents controll value in dB after Timeramp
|
||||
const newValue = ev.payload
|
||||
useNuxtApp().$logger.log('out8= ' + newValue[0])
|
||||
}
|
||||
if (ev.tag === 'out9= ') { // out3 represents controll value in dB after Timeramp
|
||||
const newValue = ev.payload
|
||||
useNuxtApp().$logger.log('Band 1000 = ' + newValue)
|
||||
this.$emit('update:control-value', newValue[0])
|
||||
}
|
||||
} catch (error: any) {
|
||||
// this.$logger.warn('Failed to attach a control value listener, music is not gain controlled.')
|
||||
}
|
||||
})
|
||||
},
|
||||
// This method helps to get the ressources free when we stop playing the audio
|
||||
// without it would be louder each time we start playing
|
||||
refreshAudioContext () {
|
||||
const newAudioContext = new AudioContext()
|
||||
this.audioContext.close()
|
||||
useAudioStore().audioContext = newAudioContext
|
||||
this.audioContext = useAudioStore().getContext()
|
||||
},
|
||||
fadeInGains () {
|
||||
this.unmute()
|
||||
// useNuxtApp().$logger.log('Fade In Gains')
|
||||
if (useAudioStore().playing !== true) { return }
|
||||
const fadeTime = this.audioContext.currentTime + 3.0
|
||||
|
||||
setTimeout(() => {
|
||||
this.fading = true
|
||||
const noiseGain = this.createdNodes.noiseGain
|
||||
noiseGain.gain.linearRampToValueAtTime(1.0, fadeTime)
|
||||
}, 450)
|
||||
setTimeout(() => {
|
||||
this.fading = false
|
||||
}, fadeTime * 1000)
|
||||
},
|
||||
fadeOutGains () {
|
||||
if (this.createdNodes.noiseGain) {
|
||||
const noiseGainValue = this.createdNodes.noiseGain.gain.value
|
||||
this.createdNodes.noiseGain.gain.linearRampToValueAtTime(noiseGainValue, this.audioContext.currentTime)
|
||||
this.createdNodes.noiseGain.gain.linearRampToValueAtTime(0, this.audioContext.currentTime + 1.3)
|
||||
}
|
||||
},
|
||||
|
||||
handleCanPlayNoise (state: boolean) {
|
||||
// useNuxtApp().$logger.log('NoiseElement has now playingstate: ' + state)
|
||||
this.noiseReady = state
|
||||
},
|
||||
readyForWebaudio () {
|
||||
if (!this.noiseReady) {
|
||||
return false
|
||||
}
|
||||
if (!this.micReady) {
|
||||
return false
|
||||
}
|
||||
if (!this.deviceReady) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
async handlePlayingUpdate2 (state: boolean) {
|
||||
useNuxtApp().$logger.log('handling Playing Update2= ' + state, this.audioContext.state)
|
||||
if (!state) {
|
||||
this.mute()
|
||||
return
|
||||
}
|
||||
if (this.readyForWebaudio()) {
|
||||
this.handlePlayingUpdate(true)
|
||||
} else {
|
||||
if (!this.deviceReady) {
|
||||
useNuxtApp().$logger.log('Device is not ready, create it now')
|
||||
await this.setupDevice()
|
||||
}
|
||||
if (!this.micReady) {
|
||||
// await this.setupMicrophone(Microphone)
|
||||
useNuxtApp().$logger.log('micophone not yet ready attach it!! ')
|
||||
// useNuxtApp().$logger.log('microphone attached' + stream)
|
||||
}
|
||||
if (this.readyForWebaudio()) {
|
||||
useNuxtApp().$logger.log('everything is now ready start play')
|
||||
this.handlePlayingUpdate(true)
|
||||
} else {
|
||||
useNuxtApp().$logger.log('Waiting for all devices to be ready')
|
||||
}
|
||||
}
|
||||
},
|
||||
handlePlayingUpdate (state: boolean) {
|
||||
try {
|
||||
// Stop the music again, mute it and set the noiseReady or musicReady to true
|
||||
if (state) {
|
||||
useNuxtApp().$logger.log('stop playing')
|
||||
const noiseElement = this.$refs.Noise as typeof AudioElement
|
||||
const noiseAudioElement = noiseElement.$refs.audioElement as HTMLMediaElement
|
||||
const audioContext = this.audioContext
|
||||
const destination = this.audioContext.destination
|
||||
audioContext.resume()
|
||||
this.createdNodes.noiseGain ||= audioContext.createGain()
|
||||
this.createdNodes.noiseGain.gain.setValueAtTime(0, audioContext.currentTime)
|
||||
|
||||
this.createdNodes.noiseSource ||= audioContext.createMediaElementSource(noiseAudioElement)
|
||||
|
||||
// HERE THE NOISE PATCH COMES INTO PLAY
|
||||
|
||||
this.createdNodes.micSplitter ||= audioContext.createChannelSplitter(2)
|
||||
this.createdNodes.noiseInputChannelSplitter ||= audioContext.createChannelSplitter(2)
|
||||
this.createdNodes.microphone.connect(this.createdNodes.micSplitter)
|
||||
this.createdNodes.noiseSource.connect(this.createdNodes.noiseInputChannelSplitter)
|
||||
this.createdNodes.micSplitter.connect(this.createdNodes.noiseDevice, 0, 0)
|
||||
this.createdNodes.noiseInputChannelSplitter.connect(this.createdNodes.noiseDevice, 0, 1)
|
||||
this.createdNodes.noiseInputChannelSplitter.connect(this.createdNodes.noiseDevice, 1, 2)
|
||||
|
||||
this.createdNodes.noiseDevice.connect(this.createdNodes.noiseGain)
|
||||
|
||||
this.createdNodes.noiseGain.connect(destination)
|
||||
this.createdNodes.noiseGain.gain.cancelScheduledValues(this.audioContext.currentTime)
|
||||
this.createdNodes.noiseGain.gain.setValueAtTime(0, this.audioContext.currentTime)
|
||||
noiseAudioElement.muted = false
|
||||
this.connected = true
|
||||
this.unmute()
|
||||
this.fadeInGains()
|
||||
useAudioStore().playing = true
|
||||
this.$logger.info('RNBO Patch successfully connected and playing')
|
||||
} else {
|
||||
// Music has just stopped react on it.
|
||||
this.$logger.info('Stopping audio and disconnecting RNBO Patch')
|
||||
this.fadeOutGains()
|
||||
this.createdNodes = []
|
||||
this.refreshAudioContext()
|
||||
this.connected = false
|
||||
}
|
||||
} catch (error) {
|
||||
this.$logger.info('Error in handlePlayingUpdate')
|
||||
this.connected = false
|
||||
useAudioStore().playing = false
|
||||
// You might want to show an error message to the user here
|
||||
}
|
||||
},
|
||||
updateNoiseGain (volume: number) {
|
||||
if (this.createdNodes.noiseGain) {
|
||||
useNuxtApp().$logger.log('volume= ' + volume)
|
||||
this.createdNodes.noiseGain.gain.linearRampToValueAtTime(volume, this.createdNodes.noiseGain.context.currentTime + 0.30)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
34
components/experiments/spotify/PomodoroPlaylist.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div>
|
||||
<iframe
|
||||
style="border-radius:12px"
|
||||
:src="spotifyEmbedUrl"
|
||||
width="100%"
|
||||
height="352"
|
||||
frameBorder="0"
|
||||
allowfullscreen=""
|
||||
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'PomodoroPlaylist',
|
||||
props: {
|
||||
spotifyUri: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
spotifyEmbedUrl () {
|
||||
if (!this.spotifyUri) {
|
||||
return 'https://open.spotify.com/embed/playlist/6HuAVqLOmYskc2qOaBZBBz?utm_source=generator'
|
||||
}
|
||||
return `https://open.spotify.com/embed/${this.spotifyUri}`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
63
components/experiments/spotify/Spotify.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div class="episodes">
|
||||
<button class="episode" data-spotify-id="spotify:episode:7makk4oTQel546B0PZlDM5">
|
||||
My Path to Spotify: Women in Engineering
|
||||
</button>
|
||||
<button class="episode" data-spotify-id="spotify:episode:43cbJh4ccRD7lzM2730YK3">
|
||||
What is Backstage?
|
||||
</button>
|
||||
<button class="episode" data-spotify-id="spotify:episode:6I3ZzCxRhRkNqnQNo8AZPV">
|
||||
Introducing Nerd Out@Spotify
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="embed-iframe" />
|
||||
</template>
|
||||
<script lang="js">
|
||||
|
||||
export default {
|
||||
name: 'SpotifyTest',
|
||||
mounted () {
|
||||
window.onSpotifyIframeApiReady = (IFrameAPI) => {
|
||||
const element = document.getElementById('embed-iframe')
|
||||
const options = {
|
||||
uri: 'spotify:episode:7makk4oTQel546B0PZlDM5'
|
||||
}
|
||||
|
||||
const callback = (EmbedController) => {}
|
||||
IFrameAPI.createController(element, options, callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style type="text/css">
|
||||
.episodes {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.episode {
|
||||
min-width: max-content;
|
||||
margin-bottom: .8rem;
|
||||
padding: .8rem 1rem;
|
||||
border-radius: 10px;
|
||||
border: 0;
|
||||
background: #191414;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.episode:hover {
|
||||
background: #1Db954;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 860px) {
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
28
components/experiments/spotify/SpotifyEmbed.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<div v-if="spotifyUri">
|
||||
<iframe
|
||||
:src="spotifyEmbedUrl"
|
||||
width="100%"
|
||||
height="380"
|
||||
frameborder="0"
|
||||
allowtransparency="true"
|
||||
allow="encrypted-media"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
spotifyUri: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
spotifyEmbedUrl () {
|
||||
return `https://open.spotify.com/embed/${this.spotifyUri}`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<h1>hell yeah</h1>
|
||||
<div @click="newDummyAudio"> NEW AUDIO</div>
|
||||
<div @click="showArtWork">SHOW ARTWORK</div>
|
||||
<div @click="play">PLAY</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, onMounted } from 'vue'
|
||||
import { useAudioStore, ensureAudio } from '~/stores/audio'
|
||||
|
||||
const createAudioTag = () => {
|
||||
const newDummyAudio = new Audio('data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU2LjM2LjEwMAAAAAAAAAAAAAAA//OEAAAAAAAAAAAAAAAAAAAAAAAASW5mbwAAAA8AAAAEAAABIADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV6urq6urq6urq6urq6urq6urq6urq6urq6v////////////////////////////////8AAAAATGF2YzU2LjQxAAAAAAAAAAAAAAAAJAAAAAAAAAAAASDs90hvAAAAAAAAAAAAAAAAAAAA//MUZAAAAAGkAAAAAAAAA0gAAAAATEFN//MUZAMAAAGkAAAAAAAAA0gAAAAARTMu//MUZAYAAAGkAAAAAAAAA0gAAAAAOTku//MUZAkAAAGkAAAAAAAAA0gAAAAANVVV')
|
||||
newDummyAudio.loop = true
|
||||
newDummyAudio.controls = true
|
||||
newDummyAudio.muted = true
|
||||
newDummyAudio.play()
|
||||
}
|
||||
|
||||
const play = (state) => {
|
||||
const audio = newDummyAudio()
|
||||
if (state) {
|
||||
audio.play().catch(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
const showArtWork = () => {
|
||||
const pathKlein = window.location.origin + '/images/scenery/noise_artwork_1024.jpg'
|
||||
const pathGross = window.location.origin + '/images/scenery/noise_artwork_512.jpg'
|
||||
navigator.mediaSession.metadata = null
|
||||
navigator.mediaSession.metadata = new MediaMetadata({
|
||||
title: 'Calm Speech Blocker',
|
||||
artist: 'mindboost',
|
||||
album: 'get your focus',
|
||||
artwork: [
|
||||
{ src: pathKlein, sizes: '1024x1024', type: 'image/jpeg' },
|
||||
{ src: pathGross, sizes: '512x512', type: 'image/jpeg' }
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
() => useAudioStore().getPlaying,
|
||||
(newVal) => {
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// Media Session Setup separat
|
||||
onMounted(async () => {
|
||||
const playState = useAudioStore().getPlaying
|
||||
await ensureAudio
|
||||
createAudioTag()
|
||||
showArtWork()
|
||||
})
|
||||
</script>
|
62
components/experiments/statemanagement/PlayButton.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<li class="nav__item">
|
||||
<a
|
||||
id="focused-icon"
|
||||
class="nav__item-link"
|
||||
href="#"
|
||||
@click.prevent="togglePlaying"
|
||||
@touchstart.prevent="togglePlaying"
|
||||
>
|
||||
<svg v-if="!playing" width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- ▶️ PLAY ICON -->
|
||||
<path
|
||||
d="m6.192 3.67 13.568 7.633a.8.8 0 0 1 0 1.394L6.192 20.33A.8.8 0 0 1 5 19.632V4.368a.8.8 0 0 1 1.192-.698Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<svg v-else width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- ⏸️ PAUSE ICON -->
|
||||
<g clip-path="url(#a)">
|
||||
<path
|
||||
d="M17.083 19.917a2.326 2.326 0 0 1-1.706-.71 2.332 2.332 0 0 1-.71-1.707V5.417c0-.665.236-1.234.71-1.706A2.333 2.333 0 0 1 17.083 3c.664 0 1.233.236 1.708.71.474.475.71 1.044.709 1.707V17.5a2.33 2.33 0 0 1-.71 1.707 2.322 2.322 0 0 1-1.707.71Zm-9.666 0a2.326 2.326 0 0 1-1.707-.71A2.332 2.332 0 0 1 5 17.5V5.417c0-.665.237-1.234.71-1.706A2.333 2.333 0 0 1 7.417 3c.663 0 1.233.236 1.707.71.475.475.71 1.044.71 1.707V17.5a2.33 2.33 0 0 1-.71 1.707 2.322 2.322 0 0 1-1.707.71Z"
|
||||
fill="#e9c046"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="a">
|
||||
<path fill="#e9c046" d="M0 0h24v24H0z" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
</template>
|
||||
<script>
|
||||
import { mapState, mapActions } from 'pinia'
|
||||
import { useAudioStore } from '~/stores/audio'
|
||||
|
||||
export default defineNuxtComponent({
|
||||
name: 'PlayButton',
|
||||
|
||||
created () {
|
||||
this.audioStore = useAudioStore()
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState(useAudioStore, ['playing'])
|
||||
},
|
||||
|
||||
watch: {
|
||||
playing (newValue) {
|
||||
this.$logger.log('Global playing state changed', newValue)
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
togglePlaying () {
|
||||
this.audioStore.setPlaying(!this.audioStore.playing)
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
154
components/experiments/statemanagement/StateBar.vue
Normal file
@@ -0,0 +1,154 @@
|
||||
<template>
|
||||
<AudioReactiveBar />
|
||||
<h3>Playing :{{ playing }}</h3>
|
||||
<h3>Number of AudioTags {{ numberOfAudioTags }}</h3>
|
||||
<p>Infos: {{ errorMessage }}</p>
|
||||
<p>AudioContext State: {{ updateCtx }}</p>
|
||||
<li v-for="(audio, index) in audioTags" :key="index">
|
||||
Tag {{ index + 1 }}: <span v-if="checkAudioElementPlaying(audio)">Playing</span><span v-else>Not Playing</span>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import { useAudioStore } from '~/stores/audio'
|
||||
import AudioReactiveBar from '~/components/AudioReactiveBar.vue'
|
||||
|
||||
export default {
|
||||
name: 'StateBar',
|
||||
components: { AudioReactiveBar },
|
||||
setup () {
|
||||
const audioStore = useAudioStore()
|
||||
const playing = ref(useAudioStore().playing)
|
||||
const audioTags = ref(Array.from(document.querySelectorAll('audio')))
|
||||
const numberOfAudioTags = ref(audioTags.value.length)
|
||||
const errorMessage = ref('')
|
||||
const updateCtx = ref(audioStore.audioContext)
|
||||
|
||||
watch(() => audioStore.playing, (newValue) => {
|
||||
playing.value = newValue
|
||||
// Run the function to log the status of audio elements
|
||||
checkAudioElementsStatus()
|
||||
})
|
||||
watch(() => numberOfAudioTags, (_newValue) => {
|
||||
// useNuxtApp().$logger.log('new AudioTag Amount' + newValue)
|
||||
// Run the function to log the status of audio elements
|
||||
checkAudioElementsStatus()
|
||||
})
|
||||
|
||||
const checkAudioElementPlaying = (audioElement:HTMLAudioElement) => {
|
||||
const isPlaying = !audioElement.paused && audioElement.currentTime > 0 && !audioElement.ended
|
||||
// useNuxtApp().$logger.log(`Audio Element ${audioElement.src}: ${isPlaying ? 'Playing' : 'Not Playing'}`)
|
||||
errorMessage.value = `Audio Element ${audioElement.src}: ${isPlaying ? 'Playing' : 'Not Playing'}`
|
||||
return isPlaying
|
||||
}
|
||||
|
||||
const monitor = () => {
|
||||
// This could be an interval or a direct method call in your gain changing methods
|
||||
setInterval(() => {
|
||||
const audioElements = document.querySelectorAll('audio')
|
||||
errorMessage.value = ''
|
||||
let currentTime, paused, ended
|
||||
audioElements.forEach((element) => {
|
||||
currentTime = element.currentTime
|
||||
paused = element.paused
|
||||
ended = element.ended
|
||||
errorMessage.value += (` Audio Element ${element.src}: time ${currentTime},paused ${paused}, ended ${ended} _______________`)
|
||||
})
|
||||
}, 100) // Update every 100 ms, adjust interval as necessary
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
monitor()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
})
|
||||
|
||||
return {
|
||||
numberOfAudioTags,
|
||||
audioTags,
|
||||
playing,
|
||||
checkAudioElementPlaying,
|
||||
errorMessage,
|
||||
monitor,
|
||||
updateCtx
|
||||
}
|
||||
}
|
||||
}
|
||||
function checkAudioElementsStatus () {
|
||||
// Find all audio elements on the page
|
||||
const audioElements = document.querySelectorAll('audio')
|
||||
|
||||
// Iterate over each audio element to check if it's playing
|
||||
audioElements.forEach((_audioElement, _index) => {
|
||||
// const _isPlaying = !audioElement.paused && audioElement.currentTime > 0 && !audioElement.ended
|
||||
// useNuxtApp().$logger.log(`Audio Element ${index + 1}: ${isPlaying ? 'Playing' : 'Not Playing'}`, { audioElement })
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
<style scoped>
|
||||
/* The switch - the box around the slider */
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 60px;
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
/* Hide default HTML checkbox */
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* The slider */
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: white;
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: #2196F3;
|
||||
}
|
||||
|
||||
input:focus + .slider {
|
||||
box-shadow: 0 0 1px #2196F3;
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
-webkit-transform: translateX(26px);
|
||||
-ms-transform: translateX(26px);
|
||||
transform: translateX(26px);
|
||||
}
|
||||
|
||||
/* Rounded sliders */
|
||||
.slider.round {
|
||||
border-radius: 34px;
|
||||
}
|
||||
|
||||
.slider.round:before {
|
||||
border-radius: 50%;
|
||||
}
|
||||
</style>
|
19
components/experiments/tests/AlgorithmusTests.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export function createMockMediaStream (): MediaStream {
|
||||
// Create an empty MediaStream
|
||||
const mockStream = new MediaStream()
|
||||
|
||||
// Create a mock audio track using the Web Audio API
|
||||
const audioContext = new AudioContext()
|
||||
const oscillator = audioContext.createOscillator()
|
||||
const dst = audioContext.createMediaStreamDestination()
|
||||
oscillator.connect(dst)
|
||||
oscillator.start()
|
||||
|
||||
// Add the audio track from the destination to the stream
|
||||
dst.stream.getAudioTracks().forEach((track) => {
|
||||
mockStream.addTrack(track)
|
||||
})
|
||||
|
||||
// You can now return this mockStream as a simulated microphone MediaStream
|
||||
return mockStream
|
||||
}
|
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<div class="noise-controlled-band">
|
||||
<AudioTag
|
||||
:ref="el => audioElement"
|
||||
:src="audioSrc"
|
||||
:volume="volume"
|
||||
:play="playing"
|
||||
@canplay="onCanPlay"
|
||||
/>
|
||||
<RNBOControlValue
|
||||
:center-frequency="centerFrequency"
|
||||
:status="playing"
|
||||
:q-factor="$props.qFactor"
|
||||
:attack="masterAttack"
|
||||
:release="masterRelease"
|
||||
@control-value-change="handleValueChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, computed } from 'vue'
|
||||
import AudioTag from '../../AudioTag.vue'
|
||||
import RNBOControlValue from '../..//tests/ControlValues/RNBOControlValue.vue'
|
||||
import tracksConfig from '~/tracks.config'
|
||||
import { useAudioStore } from '~/stores/audio'
|
||||
import { calculateNormalizedVolume } from '~/lib/AudioFunctions'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NoiseControlledBand',
|
||||
components: {
|
||||
AudioTag,
|
||||
RNBOControlValue
|
||||
},
|
||||
props: {
|
||||
centerFrequency: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
qFactor: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
masterAttack: {
|
||||
type: Number,
|
||||
default: 120000,
|
||||
required: false
|
||||
},
|
||||
masterRelease: {
|
||||
type: Number,
|
||||
default: 144000,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
emits: ['ready', 'update:mid-volume'],
|
||||
setup (props, { emit }) {
|
||||
const audioElement = ref<InstanceType<typeof HTMLElement> | null>(null)
|
||||
const playing = computed(() => { return useAudioStore().playing }) // the playing state is bind to the audioStore
|
||||
const gainValueDB = ref(0)
|
||||
const volume = ref(0)
|
||||
const audioSrc = computed(() => {
|
||||
try {
|
||||
const frequency = props.centerFrequency
|
||||
|
||||
let band = ''
|
||||
if (frequency < 500) {
|
||||
band = 'low_band'
|
||||
} else if (frequency >= 500 && frequency < 4000) {
|
||||
band = 'mid_band'
|
||||
} else {
|
||||
band = 'high_band'
|
||||
}
|
||||
|
||||
const path = `/masking/3bands/${band}_256kbps.webm`
|
||||
const fullPath = `${window.location.origin}${encodeURI(path)}`
|
||||
|
||||
useNuxtApp().$logger.info('Loading audio track:', fullPath)
|
||||
|
||||
return fullPath
|
||||
} catch (error) {
|
||||
useNuxtApp().$logger.error('Error loading audio track:', error)
|
||||
return ''
|
||||
}
|
||||
})
|
||||
|
||||
const handleValueChange = (data: { frequency: number; value: number }) => {
|
||||
// Convert dB to linear scale
|
||||
gainValueDB.value = calculateNormalizedVolume(data.value)
|
||||
volume.value = gainValueDB.value
|
||||
emit('update:mid-volume', volume.value)
|
||||
}
|
||||
|
||||
const onCanPlay = () => {
|
||||
emit('ready', props.centerFrequency)
|
||||
}
|
||||
|
||||
return {
|
||||
audioElement,
|
||||
audioSrc,
|
||||
gainValueDB,
|
||||
volume,
|
||||
handleValueChange,
|
||||
onCanPlay,
|
||||
playing
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.noise-controlled-band {
|
||||
border: 1px solid #ccc;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<div class="noise-controlled-band">
|
||||
<AudioTag
|
||||
:ref="el => audioElement"
|
||||
:src="audioSrc"
|
||||
:volume="volume"
|
||||
:play="playing"
|
||||
@canplay="onCanPlay"
|
||||
/>
|
||||
<RNBOControlValue
|
||||
:center-frequency="centerFrequency"
|
||||
:status="playing"
|
||||
:attack="masterAttack"
|
||||
:release="masterRelease"
|
||||
@control-value-change="handleValueChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, computed } from 'vue'
|
||||
import AudioTag from '../../AudioTag.vue'
|
||||
import RNBOControlValue from '../../tests/ControlValues/RNBOControlValue.vue'
|
||||
import tracksConfig from '~/tracks.config'
|
||||
import { useAudioStore } from '~/stores/audio'
|
||||
import { calculateNormalizedVolume } from '~/lib/AudioFunctions'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NoiseControlledBand',
|
||||
components: {
|
||||
AudioTag,
|
||||
RNBOControlValue
|
||||
},
|
||||
props: {
|
||||
centerFrequency: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
masterAttack: {
|
||||
type: Number,
|
||||
default: 120000,
|
||||
required: false
|
||||
},
|
||||
masterRelease: {
|
||||
type: Number,
|
||||
default: 144000,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
emits: ['ready'],
|
||||
setup (props, { emit }) {
|
||||
const audioElement = ref<InstanceType<typeof HTMLElement> | null>(null)
|
||||
const playing = computed(() => { return useAudioStore().playing }) // the playing state is bind to the audioStore
|
||||
const gainValueDB = ref(0)
|
||||
const volume = ref(0)
|
||||
const audioSrc = computed(() => {
|
||||
try {
|
||||
const frequencyKey = `${props.centerFrequency}_src`
|
||||
useNuxtApp().$logger.log('Loading audio track:', frequencyKey)
|
||||
const trackPath = tracksConfig[frequencyKey as keyof typeof tracksConfig]
|
||||
if (!trackPath) {
|
||||
throw new Error(`No track found for frequency ${props.centerFrequency}`)
|
||||
}
|
||||
const returnValue = `${window.location.origin}${encodeURI(trackPath)}`
|
||||
// Check if the audio file is available
|
||||
return returnValue
|
||||
} catch (error) {
|
||||
useNuxtApp().$logger.error('Error loading audio track:', error)
|
||||
return ''
|
||||
}
|
||||
})
|
||||
|
||||
const handleValueChange = (data: { frequency: number; value: number }) => {
|
||||
// Convert dB to linear scale
|
||||
|
||||
gainValueDB.value = calculateNormalizedVolume(data.value)
|
||||
volume.value = gainValueDB.value
|
||||
}
|
||||
|
||||
const onCanPlay = () => {
|
||||
useNuxtApp().$logger.log(`Audio for frequency ${props.centerFrequency} is ready to play`)
|
||||
emit('ready', props.centerFrequency)
|
||||
}
|
||||
|
||||
return {
|
||||
audioElement,
|
||||
audioSrc,
|
||||
gainValueDB,
|
||||
volume,
|
||||
handleValueChange,
|
||||
onCanPlay,
|
||||
playing
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.noise-controlled-band {
|
||||
border: 1px solid #ccc;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<div v-show="false" id="hiddenAudio">
|
||||
<AudioTagWebAudio
|
||||
ref="audioElement"
|
||||
:src="audioSrc"
|
||||
:volume="volume"
|
||||
:play="playing"
|
||||
:master-gain="masterGain"
|
||||
@canplay="onCanPlay"
|
||||
/>
|
||||
<div class="noise-controlled-band">
|
||||
Gain: {{ gainValueDB }}
|
||||
<RNBOControlValue
|
||||
:center-frequency="centerFrequency"
|
||||
:status="playing"
|
||||
:attack="masterAttack"
|
||||
:release="masterRelease"
|
||||
:q-factor="qFactor"
|
||||
@control-value-change="handleValueChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, computed } from 'vue'
|
||||
import type { Logger } from 'pino'
|
||||
import RNBOControlValue from '../../tests/ControlValues/RNBOControlValue.vue'
|
||||
import AudioTagWebAudio from '../../AudioTagWebAudio.vue'
|
||||
import { calculateNormalizedVolume } from '~/lib/AudioFunctions'
|
||||
import { useAudioStore } from '~/stores/audio'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NoiseControlledWebAudio3Band',
|
||||
components: {
|
||||
AudioTagWebAudio,
|
||||
RNBOControlValue
|
||||
},
|
||||
props: {
|
||||
centerFrequency: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
masterGain: {
|
||||
type: GainNode,
|
||||
required: true
|
||||
},
|
||||
masterAttack: {
|
||||
type: Number,
|
||||
default: 120000 * 2,
|
||||
required: false
|
||||
},
|
||||
masterRelease: {
|
||||
type: Number,
|
||||
default: 144000 / 1000,
|
||||
required: false
|
||||
},
|
||||
qFactor: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['ready'],
|
||||
setup (props, { emit }) {
|
||||
const audioElement = ref<InstanceType<typeof HTMLElement> | null>(null)
|
||||
const gainValueDB = ref(0)
|
||||
const volume = ref(1)
|
||||
const playing = computed(() => { return useAudioStore().getPlaying }) // the playing state is bind to the audioStore
|
||||
const logger = useNuxtApp().$logger as Logger
|
||||
const audioSrc = computed(() => {
|
||||
try {
|
||||
const frequency = props.centerFrequency
|
||||
|
||||
let band = ''
|
||||
if (frequency < 500) {
|
||||
band = 'low_band'
|
||||
} else if (frequency >= 500 && frequency < 4000) {
|
||||
band = 'mid_band'
|
||||
} else {
|
||||
band = 'high_band'
|
||||
}
|
||||
|
||||
const path = `/masking/3bands/${band}_256kbps.webm`
|
||||
const fullPath = `${window.location.origin}${encodeURI(path)}`
|
||||
return fullPath
|
||||
} catch (error) {
|
||||
return ''
|
||||
}
|
||||
})
|
||||
const handleValueChange = (data: { frequency: number; value: number }) => {
|
||||
// Convert dB to linear scale
|
||||
gainValueDB.value = calculateNormalizedVolume(data.value)
|
||||
volume.value = gainValueDB.value
|
||||
}
|
||||
|
||||
const onCanPlay = () => {
|
||||
logger.info(`Audio for frequency ${props.centerFrequency} is ready to play`)
|
||||
emit('ready', props.centerFrequency)
|
||||
}
|
||||
|
||||
return {
|
||||
audioElement,
|
||||
audioSrc,
|
||||
gainValueDB,
|
||||
volume,
|
||||
handleValueChange,
|
||||
onCanPlay,
|
||||
playing
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.noise-controlled-band {
|
||||
border: 1px solid #ccc;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<div class="noise-controlled-band">
|
||||
<AudioTagWebAudio
|
||||
ref="audioElement"
|
||||
:src="audioSrc"
|
||||
:volume="volume"
|
||||
:play="playing"
|
||||
:master-gain="masterGain"
|
||||
@canplay="onCanPlay"
|
||||
/>
|
||||
Gain: {{ gainValueDB }}
|
||||
<RNBOControlValue
|
||||
:center-frequency="centerFrequency"
|
||||
:status="playing"
|
||||
:attack="masterAttack"
|
||||
:release="masterRelease"
|
||||
@control-value-change="handleValueChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, computed } from 'vue'
|
||||
import type { Logger } from 'pino'
|
||||
import RNBOControlValue from '../..//tests/ControlValues/RNBOControlValue.vue'
|
||||
import AudioTagWebAudio from '../../AudioTagWebAudio.vue'
|
||||
import tracksConfig from '~/tracks.config'
|
||||
import { calculateNormalizedVolume } from '~/lib/AudioFunctions'
|
||||
import { useAudioStore } from '~/stores/audio'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NoiseControlledWebAudioBand',
|
||||
components: {
|
||||
AudioTagWebAudio,
|
||||
RNBOControlValue
|
||||
},
|
||||
props: {
|
||||
centerFrequency: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
masterGain: {
|
||||
type: GainNode,
|
||||
required: true
|
||||
},
|
||||
masterAttack: {
|
||||
type: Number,
|
||||
default: 120000,
|
||||
required: false
|
||||
},
|
||||
masterRelease: {
|
||||
type: Number,
|
||||
default: 144000,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
emits: ['ready'],
|
||||
setup (props, { emit }) {
|
||||
const audioElement = ref<InstanceType<typeof HTMLElement> | null>(null)
|
||||
const gainValueDB = ref(0)
|
||||
const volume = ref(1)
|
||||
const playing = computed(() => { return useAudioStore().playing }) // the playing state is bind to the audioStore
|
||||
const logger = useNuxtApp().$logger as Logger
|
||||
const audioSrc = computed(() => {
|
||||
try {
|
||||
const frequencyKey = `${props.centerFrequency}_src`
|
||||
logger.info('Loading audio track:', frequencyKey)
|
||||
const trackPath = tracksConfig[frequencyKey as keyof typeof tracksConfig]
|
||||
if (!trackPath) {
|
||||
throw new Error(`No track found for frequency ${props.centerFrequency}`)
|
||||
}
|
||||
const returnValue = `${window.location.origin}${encodeURI(trackPath)}`
|
||||
// Check if the audio file is available
|
||||
return returnValue
|
||||
} catch (error) {
|
||||
logger.error('Error loading audio track:', error)
|
||||
return ''
|
||||
}
|
||||
})
|
||||
const handleValueChange = (data: { frequency: number; value: number }) => {
|
||||
// Convert dB to linear scale
|
||||
gainValueDB.value = calculateNormalizedVolume(data.value)
|
||||
volume.value = gainValueDB.value
|
||||
}
|
||||
|
||||
const onCanPlay = () => {
|
||||
logger.info(`Audio for frequency ${props.centerFrequency} is ready to play`)
|
||||
emit('ready', props.centerFrequency)
|
||||
}
|
||||
|
||||
return {
|
||||
audioElement,
|
||||
audioSrc,
|
||||
gainValueDB,
|
||||
volume,
|
||||
handleValueChange,
|
||||
onCanPlay,
|
||||
playing
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.noise-controlled-band {
|
||||
border: 1px solid #ccc;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
</style>
|
316
components/experiments/tests/ControlValues/RNBOControlValue.vue
Normal file
@@ -0,0 +1,316 @@
|
||||
<!--
|
||||
RNBOControlValue ist eine Komponente zur Steuerung und Überwachung von Audio-Parametern,
|
||||
insbesondere für die Kontrolle von Frequenzbändern. Sie ermöglicht das Testen von
|
||||
Audio-Geräten, die Anpassung von Parametern und die Anzeige von Kontrollwerten in Echtzeit.
|
||||
-->
|
||||
<template>
|
||||
<div v-if="true">
|
||||
<h2>Control Values Device Test -- {{ centerFrequency }}Hz</h2>
|
||||
<button @click="testControlValuesDevice">Test Control Values Device</button>
|
||||
<p v-if="testResult">{{ testResult }}</p>
|
||||
<div class="microphone-info">
|
||||
<h3>Current Microphone:</h3>
|
||||
<p>{{ currentMicrophone || 'No microphone selected' }}</p>
|
||||
</div>
|
||||
|
||||
<table v-if="parameters.length > 0" class="parameter-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Parameter Name</th>
|
||||
<th>Value</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="param in parameters" :key="param.name">
|
||||
<td>{{ param.name }}</td>
|
||||
<td>
|
||||
<input
|
||||
:value="param.value"
|
||||
type="number"
|
||||
@input="updateParameter(param.name, $event.target?.value)"
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
:value="param.value"
|
||||
type="range"
|
||||
:min="param.min"
|
||||
:max="param.max"
|
||||
:step="param.step"
|
||||
@input="updateParameter(param.name, $event.target?.value)"
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- Neue Keynote für den Outlet-Wert -->
|
||||
<div class="outlet-keynote">
|
||||
<h3>Control Value</h3>
|
||||
<div class="outlet-value">{{ formatValue(outletValue) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import { mapActions, mapState } from 'pinia'
|
||||
import { useDevicesStore } from '~/stores/device'
|
||||
import { useMicStore } from '~/stores/microphone'
|
||||
import { useAudioStore } from '~/stores/audio'
|
||||
|
||||
interface ParameterData {
|
||||
name: string;
|
||||
paramId: string;
|
||||
value: number;
|
||||
min: number;
|
||||
max: number;
|
||||
step: number;
|
||||
}
|
||||
|
||||
const minSamples = 48
|
||||
const maxSamples = 1920000
|
||||
|
||||
export default defineComponent({
|
||||
name: 'RNBOControlValue',
|
||||
props: {
|
||||
centerFrequency: {
|
||||
type: Number,
|
||||
required: true,
|
||||
validator: (value: number) => [63, 125, 250, 500, 1000, 1500, 2000, 4000, 8000, 16000, 150].includes(value)
|
||||
},
|
||||
qFactor: {
|
||||
type: Number,
|
||||
default: 1.414,
|
||||
validator: (value: number) => value > 0.1 && value < 1.5
|
||||
},
|
||||
attack: {
|
||||
type: Number,
|
||||
default: 120000,
|
||||
validator: (value:number) => value >= minSamples && value <= maxSamples
|
||||
},
|
||||
release: {
|
||||
type: Number,
|
||||
default: 144000,
|
||||
validator: (value:number) => value >= minSamples && value <= maxSamples
|
||||
},
|
||||
status: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['control-value-change'],
|
||||
data () {
|
||||
return {
|
||||
testResult: '',
|
||||
parameters: [] as ParameterData[],
|
||||
device: null as any,
|
||||
outletValue: 0,
|
||||
currentMicrophone: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(useMicStore, ['microphone'])
|
||||
},
|
||||
watch: {
|
||||
release (value) {
|
||||
this.updateParameter('release', '' + value)
|
||||
},
|
||||
attack (value) {
|
||||
if (!isNaN(value)) {
|
||||
this.updateParameter('attack', '' + value)
|
||||
}
|
||||
},
|
||||
status (newStatus) {
|
||||
const microphoneStore = useMicStore()
|
||||
// status is bind to playing status in audio store
|
||||
if (newStatus) {
|
||||
microphoneStore.attachMicrophone()
|
||||
this.testControlValuesDevice()
|
||||
} else {
|
||||
microphoneStore.detachMicrophone()
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
mounted () {
|
||||
this.updateParameter('centerFrequency', '' + this.centerFrequency)
|
||||
this.updateParameter('release', '' + this.release)
|
||||
this.updateParameter('attack', '' + this.attack)
|
||||
this.updateParameter('qFactor', String(this.qFactor))
|
||||
},
|
||||
methods: {
|
||||
async getCurrentMicrophone () {
|
||||
const micStore = useMicStore()
|
||||
|
||||
try {
|
||||
const micro = await micStore.getMicrophone()
|
||||
if (micro?.microphoneNode.label) {
|
||||
this.currentMicrophone = micro.microphoneNode.label
|
||||
} else {
|
||||
this.currentMicrophone = 'No microphone detected'
|
||||
}
|
||||
} catch (error) {
|
||||
useNuxtApp().$logger.error('Error getting microphone:', error)
|
||||
this.currentMicrophone = 'Error detecting microphone'
|
||||
}
|
||||
},
|
||||
async testControlValuesDevice () {
|
||||
const deviceStore = useDevicesStore()
|
||||
const micStore = useMicStore()
|
||||
const audioStore = useAudioStore()
|
||||
|
||||
await audioStore.getContext()
|
||||
if (!audioStore) { return }
|
||||
if (audioStore.audioContext?.state !== 'running') {
|
||||
await audioStore.audioContext?.resume()
|
||||
}
|
||||
this.device = await deviceStore.createControlValuesDevice(`testControlValues_${this.centerFrequency}Hz`, this.centerFrequency)
|
||||
if (!this.device) {
|
||||
this.testResult = `Failed to create control values device for ${this.centerFrequency}Hz`
|
||||
// this.$logger.error('Device creation failed')
|
||||
}
|
||||
const device = this.device
|
||||
// this.$logger.info(`Created device for ${this.centerFrequency}Hz`, { device })
|
||||
const microphone = await micStore.getMicrophone()
|
||||
|
||||
try {
|
||||
const micSource = microphone.microphoneNode
|
||||
if (micSource && this.device.node && audioStore.audioContext) {
|
||||
micSource.connect(this.device.node)
|
||||
await this.updateParameterList()
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
|
||||
this.setupOutletListener()
|
||||
this.testResult = `Control values device for ${this.centerFrequency}Hz created successfully`
|
||||
} else {
|
||||
this.testResult = `Failed to connect microphone to device for ${this.centerFrequency}Hz`
|
||||
// this.$logger.error('Connection failed:', { micSource, deviceNode: this.device.node, audioContext: audioStore.audioContext })
|
||||
}
|
||||
} catch (error) {
|
||||
// this.$logger.error(`Test failed for ${this.centerFrequency}Hz:`, error)
|
||||
this.testResult = `Test failed for ${this.centerFrequency}Hz: ${error instanceof Error ? error.message : String(error)}`
|
||||
// this.$logger.error(this.testResult)
|
||||
}
|
||||
},
|
||||
setupOutletListener () {
|
||||
try {
|
||||
if (!this.device) {
|
||||
// this.$logger.warn('Device is not available')
|
||||
return
|
||||
}
|
||||
this.device.messageEvent.subscribe((ev: any) => {
|
||||
if (ev.tag === 'out1') {
|
||||
const newValue = this.ensureNumber(ev.payload)
|
||||
this.outletValue = newValue
|
||||
if (!(newValue > -13 && newValue < -12.98)) {
|
||||
this.$emit('control-value-change', { frequency: this.centerFrequency, value: newValue })
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (error: unknown) {
|
||||
// this.$logger.error(`Test failed for ${this.centerFrequency}Hz:`, error)
|
||||
this.testResult = `Test failed for ${this.centerFrequency}Hz: ${error instanceof Error ? error.message : String(error)}`
|
||||
}
|
||||
},
|
||||
updateParameterList () {
|
||||
// this.$logger.info('Updating parameter list', this.device)
|
||||
if (this.device && this.device.parameters) {
|
||||
const params = this.device.parameters
|
||||
// this.$logger.info('Parameters:', { params })
|
||||
|
||||
this.parameters = Array.from(params.entries()).map(([name, param]: [string, any]) => {
|
||||
return {
|
||||
name: param.name,
|
||||
value: this.ensureNumber(param.value),
|
||||
min: this.ensureNumber(param.min),
|
||||
max: this.ensureNumber(param.max),
|
||||
step: this.ensureNumber(param.step) || 0.01
|
||||
}
|
||||
})
|
||||
|
||||
// this.$logger.info('Updated parameters:', this.parameters)
|
||||
} else {
|
||||
// this.$logger.info('No parameters found')
|
||||
}
|
||||
},
|
||||
updateParameter (name: string, value: string) {
|
||||
if (this.device && this.device.parameters) {
|
||||
const numValue = this.ensureNumber(value)
|
||||
const param = this.parameters.find(p => p.name === name)
|
||||
|
||||
if (param) {
|
||||
param.value = numValue
|
||||
|
||||
if (typeof this.device.parameters.set === 'function') {
|
||||
this.device.parameters.set(name, numValue)
|
||||
} else if (Array.isArray(this.device.parameters)) {
|
||||
const deviceParam = this.device.parameters.find((p: any) => p.name === name)
|
||||
if (deviceParam) {
|
||||
deviceParam.value = numValue
|
||||
}
|
||||
} else if (typeof this.device.parameters === 'object') {
|
||||
if (this.device.parameters[name]) {
|
||||
this.device.parameters[name].value = numValue
|
||||
}
|
||||
}
|
||||
this.$forceUpdate()
|
||||
}
|
||||
} else {
|
||||
// Device noch nicht da → update nur in local parameters
|
||||
const param = this.parameters.find(p => p.name === name)
|
||||
if (param) {
|
||||
param.value = this.ensureNumber(value)
|
||||
} else {
|
||||
this.parameters.push({ name, value: this.ensureNumber(value) })
|
||||
}
|
||||
this.$forceUpdate()
|
||||
}
|
||||
},
|
||||
ensureNumber (value: any): number {
|
||||
const num = Number(value)
|
||||
return isNaN(num) ? 0 : num
|
||||
},
|
||||
formatValue (value: number): string {
|
||||
return value.toFixed(2)
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.parameter-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.parameter-table th, .parameter-table td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.parameter-table th {
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
|
||||
.parameter-table input[type="range"] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.outlet-keynote {
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.outlet-keynote h3 {
|
||||
font-size: 1.2em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.outlet-value {
|
||||
font-size: 2em;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
75
components/experiments/tests/Microphone.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div v-show="false">
|
||||
<h1>Microphone</h1>
|
||||
<button @click="attach">
|
||||
{{ microphoneActive ? 'Mikrofon trennen' : 'Mikrofon aktivieren' }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useAudioStore } from '~/stores/audio'
|
||||
import { useMicStore } from '~/stores/microphone'
|
||||
export default {
|
||||
name: 'MicrophoneHandler',
|
||||
emits: ['update:attach'],
|
||||
setup (_props, { emit }) {
|
||||
const audioStore = useAudioStore()
|
||||
const microphone = ref<Promise<MediaStream> | null>(null)
|
||||
const microphoneActive = ref(false)
|
||||
|
||||
const attach = () => {
|
||||
if (!microphone.value) {
|
||||
microphone.value = navigator.mediaDevices.getUserMedia({
|
||||
audio: {
|
||||
echoCancellation: false,
|
||||
noiseSuppression: false,
|
||||
autoGainControl: false
|
||||
},
|
||||
video: false
|
||||
})
|
||||
microphoneActive.value = true
|
||||
}
|
||||
|
||||
return microphone.value.then((stream) => {
|
||||
// useNuxtApp().$logger.log('stream there' + stream)
|
||||
emit('update:attach', stream)
|
||||
return stream
|
||||
})
|
||||
}
|
||||
|
||||
const detach = async () => {
|
||||
if (microphone.value) {
|
||||
try {
|
||||
const stream = await microphone.value
|
||||
stream.getTracks().forEach(track => track.stop())
|
||||
} catch (error) {
|
||||
}
|
||||
microphone.value = null
|
||||
microphoneActive.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => audioStore.playing, (newValue) => {
|
||||
if (newValue) {
|
||||
attach()
|
||||
} else {
|
||||
detach()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// Clean up by detaching the microphone when the component is unmounted
|
||||
detach()
|
||||
})
|
||||
|
||||
// Return the public properties and methods
|
||||
return {
|
||||
attach,
|
||||
detach,
|
||||
isPlaying: () => audioStore.playing,
|
||||
microphoneActive
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
83
components/experiments/tests/NoiseMusicGain.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<h1>Test Version NoiseMusicGain: mit WebAudio & Gain, ohne Noise-Patch & ohne Music-Patch</h1>
|
||||
<AudioElement
|
||||
ref="Noise"
|
||||
key="5"
|
||||
:src="noise_src"
|
||||
title="Noise"
|
||||
@update:volume="updateNoiseGain"
|
||||
>
|
||||
<template #default="{}">
|
||||
<div class="icon">
|
||||
<img style="width: 25px" src="~/assets/image/noiseicon.svg">
|
||||
</div>
|
||||
</template>
|
||||
</AudioElement>
|
||||
<AudioElement
|
||||
ref="Music"
|
||||
key="1"
|
||||
:src="forest_src"
|
||||
title="Forest"
|
||||
@update:volume="updateMusicGain"
|
||||
@update:playing="handlePlayingUpdate"
|
||||
>
|
||||
<template #default="{ }">
|
||||
<div class="icon">
|
||||
<!-- tropic icon -->
|
||||
<img style="width: 25px" src="~/assets/image/musicicon.svg">
|
||||
</div>
|
||||
</template>
|
||||
<!-- Slot content for AudioElement, if needed -->
|
||||
</AudioElement>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import AudioElement from '../AudioElement.vue'
|
||||
import { useAudioStore } from '../../../stores/audio'
|
||||
|
||||
export default {
|
||||
name: 'NoiseMusicGain',
|
||||
components: { AudioElement },
|
||||
data () {
|
||||
return {
|
||||
audioContext: useAudioStore().getContext(),
|
||||
playing: false,
|
||||
paused: false,
|
||||
createdNodes: {} as any,
|
||||
noise_src: window.location.origin + useRuntimeConfig().public.noise_src,
|
||||
forest_src: window.location.origin + useRuntimeConfig().public.tracks.forest_src
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handlePlayingUpdate () {
|
||||
const noiseElement = this.$refs.Noise as typeof AudioElement
|
||||
const noiseAudioElement = noiseElement.$refs.audioElement as HTMLMediaElement
|
||||
const musicElement = this.$refs.Music as typeof AudioElement
|
||||
const musicAudioElement = musicElement.$refs.audioElement as HTMLMediaElement
|
||||
const audioContext = this.audioContext
|
||||
const destination = this.audioContext.destination
|
||||
|
||||
this.createdNodes.musicGain ||= audioContext.createGain()
|
||||
this.createdNodes.noiseGain ||= audioContext.createGain()
|
||||
|
||||
this.createdNodes.noiseSource = audioContext.createMediaElementSource(noiseAudioElement)
|
||||
this.createdNodes.musicSource = audioContext.createMediaElementSource(musicAudioElement)
|
||||
|
||||
this.createdNodes.noiseSource.connect(this.createdNodes.noiseGain)
|
||||
this.createdNodes.musicSource.connect(this.createdNodes.musicGain)
|
||||
|
||||
this.createdNodes.noiseGain.connect(destination)
|
||||
this.createdNodes.musicGain.connect(destination)
|
||||
musicAudioElement.muted = false
|
||||
noiseAudioElement.muted = false
|
||||
},
|
||||
|
||||
updateNoiseGain (volume: number) {
|
||||
this.createdNodes.noiseGain.gain.linearRampToValueAtTime(volume, this.createdNodes.noiseGain.context.currentTime + 0.30)
|
||||
},
|
||||
updateMusicGain (volume: number) {
|
||||
this.createdNodes.musicGain.gain.linearRampToValueAtTime(volume, this.createdNodes.musicGain.context.currentTime + 0.30)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
99
components/experiments/tests/NoiseMusicGainFadeIn.vue
Normal file
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<h1>Test Version NoiseMusicGain and FadeIn: mit WebAudio & Gain, ohne Noise-Patch & ohne Music-Patch</h1>
|
||||
<h2> Obwohl die Methode linearRampToValueAtTime verwendet wird, startet das audio einfach nach der eingestellten Zeit ohne fade</h2>
|
||||
<button @click="fadeInGains">
|
||||
Trigger FadeIn
|
||||
</button>
|
||||
<button @click="fadeOutGains">
|
||||
Trigger FadeOut
|
||||
</button>
|
||||
<AudioElement
|
||||
ref="Noise"
|
||||
key="5"
|
||||
:src="noise_src"
|
||||
title="Noise"
|
||||
@update:volume="updateNoiseGain"
|
||||
>
|
||||
<template #default="{}">
|
||||
<img style="width: 25px" src="~/assets/image/noiseicon.svg">
|
||||
</template>
|
||||
</AudioElement>
|
||||
<AudioElement
|
||||
ref="Music"
|
||||
key="1"
|
||||
:src="forest_src"
|
||||
title="Forest"
|
||||
@update:volume="updateMusicGain"
|
||||
@update:playing="handlePlayingUpdate"
|
||||
>
|
||||
<template #default="{ }">
|
||||
<img style="width: 25px" src="~/assets/image/musicicon.svg">
|
||||
</template>
|
||||
</AudioElement>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import AudioElement from '../AudioElement.vue'
|
||||
import { useAudioStore } from '../../../stores/audio'
|
||||
|
||||
export default {
|
||||
name: 'NoiseMusicGain',
|
||||
components: { AudioElement },
|
||||
data () {
|
||||
return {
|
||||
audioContext: useAudioStore().getContext(),
|
||||
playing: false,
|
||||
paused: false,
|
||||
createdNodes: {} as any,
|
||||
forest_src: window.location.origin + useRuntimeConfig().public.tracks.forest_src as string,
|
||||
noise_src: window.location.origin + useRuntimeConfig().public.noise_src as string
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handlePlayingUpdate () {
|
||||
const noiseElement = this.$refs.Noise as typeof AudioElement
|
||||
const noiseAudioElement = noiseElement.$refs.audioElement as HTMLMediaElement
|
||||
const musicElement = this.$refs.Music as typeof AudioElement
|
||||
const musicAudioElement = musicElement.$refs.audioElement as HTMLMediaElement
|
||||
const audioContext = this.audioContext
|
||||
const destination = this.audioContext.destination
|
||||
|
||||
this.createdNodes.musicGain ||= audioContext.createGain()
|
||||
this.createdNodes.noiseGain ||= audioContext.createGain()
|
||||
|
||||
this.createdNodes.musicGain.gain.setValueAtTime(0, audioContext.currentTime)
|
||||
this.createdNodes.noiseGain.gain.setValueAtTime(0, audioContext.currentTime)
|
||||
|
||||
musicAudioElement.muted = false
|
||||
noiseAudioElement.muted = false
|
||||
|
||||
this.createdNodes.noiseSource = audioContext.createMediaElementSource(noiseAudioElement)
|
||||
this.createdNodes.musicSource = audioContext.createMediaElementSource(musicAudioElement)
|
||||
|
||||
this.createdNodes.noiseSource.connect(this.createdNodes.noiseGain)
|
||||
this.createdNodes.musicSource.connect(this.createdNodes.musicGain)
|
||||
|
||||
this.createdNodes.noiseGain.connect(destination)
|
||||
this.createdNodes.musicGain.connect(destination)
|
||||
|
||||
this.fadeInGains()
|
||||
},
|
||||
fadeInGains () {
|
||||
const noiseGain = this.createdNodes.noiseGain
|
||||
const musicGain = this.createdNodes.musicGain
|
||||
noiseGain.gain.linearRampToValueAtTime(1.0, noiseGain.context.currentTime + 7)
|
||||
musicGain.gain.linearRampToValueAtTime(1.0, musicGain.context.currentTime + 5)
|
||||
},
|
||||
fadeOutGains () {
|
||||
this.createdNodes.noiseGain.gain.linearRampToValueAtTime(0, this.createdNodes.noiseGain.context.currentTime + 3)
|
||||
this.createdNodes.musicGain.gain.linearRampToValueAtTime(0, this.createdNodes.musicGain.context.currentTime + 3)
|
||||
},
|
||||
updateNoiseGain (volume: number) {
|
||||
this.createdNodes.noiseGain.gain.linearRampToValueAtTime(volume, this.createdNodes.noiseGain.context.currentTime + 0.30)
|
||||
},
|
||||
updateMusicGain (volume: number) {
|
||||
this.createdNodes.musicGain.gain.linearRampToValueAtTime(volume, this.createdNodes.musicGain.context.currentTime + 0.30)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
90
components/experiments/tests/NoiseMusicGainMic.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<h1>Test Version NoiseMusicGain: mit WebAudio & Gain, ohne Noise-Patch & ohne Music-Patch</h1>
|
||||
<MicrophoneHandler ref="Microphone" @update:attach="setupMicrophone" />
|
||||
<AudioElement
|
||||
ref="Noise"
|
||||
key="5"
|
||||
:src="noise_src"
|
||||
title="Noise"
|
||||
@update:volume="updateNoiseGain"
|
||||
>
|
||||
<template #default="{}">
|
||||
<div class="icon">
|
||||
<img style="width: 25px" src="~/assets/image/noiseicon.svg">
|
||||
</div>
|
||||
</template>
|
||||
</AudioElement>
|
||||
<AudioElement
|
||||
ref="Music"
|
||||
key="1"
|
||||
:src="forest_src"
|
||||
title="Forest"
|
||||
@update:volume="updateMusicGain"
|
||||
@update:playing="handlePlayingUpdate"
|
||||
>
|
||||
<template #default="{ }">
|
||||
<div class="icon">
|
||||
<!-- tropic icon -->
|
||||
<img style="width: 25px" src="~/assets/image/musicicon.svg">
|
||||
</div>
|
||||
</template>
|
||||
<!-- Slot content for AudioElement, if needed -->
|
||||
</AudioElement>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import AudioElement from '../AudioElement.vue'
|
||||
import { useAudioStore } from '../../../stores/audio'
|
||||
import MicrophoneHandler from '../tests/Microphone.vue'
|
||||
|
||||
export default {
|
||||
name: 'NoiseMusicGain',
|
||||
components: { AudioElement, MicrophoneHandler },
|
||||
data () {
|
||||
return {
|
||||
audioContext: useAudioStore().getContext(),
|
||||
playing: false,
|
||||
paused: false,
|
||||
createdNodes: {} as any,
|
||||
forest_src: window.location.origin + useRuntimeConfig().public.tracks.forest_src as string,
|
||||
noise_src: window.location.origin + useRuntimeConfig().public.noise_src as string
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setupMicrophone (stream:MediaStream) {
|
||||
// useNuxtApp().$logger.log('setupMicrophone got this stream', { stream })
|
||||
this.createdNodes.microphone = this.audioContext.createMediaStreamSource(stream)
|
||||
this.createdNodes.microphone.connect(this.audioContext.destination)
|
||||
},
|
||||
handlePlayingUpdate () {
|
||||
const noiseElement = this.$refs.Noise as typeof AudioElement
|
||||
const noiseAudioElement = noiseElement.$refs.audioElement as HTMLMediaElement
|
||||
const musicElement = this.$refs.Music as typeof AudioElement
|
||||
const musicAudioElement = musicElement.$refs.audioElement as HTMLMediaElement
|
||||
const audioContext = this.audioContext
|
||||
const destination = this.audioContext.destination
|
||||
|
||||
this.createdNodes.musicGain ||= audioContext.createGain()
|
||||
this.createdNodes.noiseGain ||= audioContext.createGain()
|
||||
|
||||
this.createdNodes.noiseSource = audioContext.createMediaElementSource(noiseAudioElement)
|
||||
this.createdNodes.musicSource = audioContext.createMediaElementSource(musicAudioElement)
|
||||
|
||||
this.createdNodes.noiseSource.connect(this.createdNodes.noiseGain)
|
||||
this.createdNodes.musicSource.connect(this.createdNodes.musicGain)
|
||||
|
||||
this.createdNodes.noiseGain.connect(destination)
|
||||
this.createdNodes.musicGain.connect(destination)
|
||||
musicAudioElement.muted = false
|
||||
noiseAudioElement.muted = false
|
||||
},
|
||||
|
||||
updateNoiseGain (volume: number) {
|
||||
this.createdNodes.noiseGain.gain.linearRampToValueAtTime(volume, this.createdNodes.noiseGain.context.currentTime + 0.30)
|
||||
},
|
||||
updateMusicGain (volume: number) {
|
||||
this.createdNodes.musicGain.gain.linearRampToValueAtTime(volume, this.createdNodes.musicGain.context.currentTime + 0.30)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
220
components/experiments/tests/NoiseMusicGainPlayPause.vue
Normal file
@@ -0,0 +1,220 @@
|
||||
<template>
|
||||
<h1>Test Version NoiseMusicGain: mit WebAudio & Gain und PlayPause, ohne Noise-Patch & ohne Music-Patch</h1>
|
||||
<h2>Play State: {{ playing }} </h2>
|
||||
<p>
|
||||
The method refreshAudioContext helps to get the ressources free when we stop playing the audio
|
||||
// without it would be louder each time we start playing
|
||||
</p>
|
||||
<p>
|
||||
Whenever I view this page the audio starts playing, when I hit 'space' it fades out within 2seconds
|
||||
when i start playing again nothing happens... I would expect playing.
|
||||
</p>
|
||||
|
||||
<div v-if="createdNodes.musicGain">
|
||||
{{ musicGain }}
|
||||
</div>
|
||||
<div v-else>
|
||||
No MusicGain
|
||||
</div>
|
||||
<div v-if="createdNodes.noiseGain">
|
||||
{{ noiseGain }}
|
||||
</div>
|
||||
<div v-else>
|
||||
No noiseGain
|
||||
</div>
|
||||
<AudioElement
|
||||
ref="Noise"
|
||||
key="5"
|
||||
:src="noise_src"
|
||||
title="Noise"
|
||||
@update:volume="updateNoiseGain"
|
||||
@update:loaded="noiseReady=true"
|
||||
>
|
||||
<template #default="{}">
|
||||
<div class="icon">
|
||||
<img style="width: 25px" src="~/assets/image/noiseicon.svg">
|
||||
</div>
|
||||
</template>
|
||||
</AudioElement>
|
||||
<AudioElement
|
||||
ref="Music"
|
||||
key="1"
|
||||
:src="forest_src"
|
||||
title="Forest"
|
||||
@update:volume="updateMusicGain"
|
||||
@update:playing="handlePlayingUpdate"
|
||||
@update:fadeout="fadeOutGains"
|
||||
@update:loaded="musicReady=true"
|
||||
>
|
||||
<template #default="{ }">
|
||||
<div class="icon">
|
||||
<!-- tropic icon -->
|
||||
<img style="width: 25px" src="~/assets/image/musicicon.svg">
|
||||
</div>
|
||||
</template>
|
||||
<!-- Slot content for AudioElement, if needed -->
|
||||
</AudioElement>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import AudioElement from '../AudioElement2.vue'
|
||||
import { useAudioStore } from '../../../stores/audio'
|
||||
export default {
|
||||
name: 'NoiseMusicGain',
|
||||
components: { AudioElement },
|
||||
|
||||
data () {
|
||||
return {
|
||||
audioContext: useAudioStore().getContext(),
|
||||
playing: false,
|
||||
paused: false,
|
||||
createdNodes: {} as any,
|
||||
fading: false,
|
||||
noiseReady: false,
|
||||
musicReady: false,
|
||||
musicGain: 0,
|
||||
noiseGain: 0,
|
||||
forest_src: window.location.origin + useRuntimeConfig().public.tracks.forest_src as string,
|
||||
noise_src: window.location.origin + useRuntimeConfig().public.noise_src as string
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.monitorGainNodes()
|
||||
},
|
||||
methods: {
|
||||
monitorGainNodes () {
|
||||
// This could be an interval or a direct method call in your gain changing methods
|
||||
setInterval(() => {
|
||||
if (this.createdNodes.musicGain) {
|
||||
this.musicGain = this.createdNodes.musicGain.gain.value
|
||||
}
|
||||
if (this.createdNodes.noiseGain) {
|
||||
this.noiseGain = this.createdNodes.noiseGain.gain.value
|
||||
}
|
||||
}, 100) // Update every 100 ms, adjust interval as necessary
|
||||
},
|
||||
// This method helps to get the ressources free when we stop playing the audio
|
||||
// without it would be louder each time we start playing
|
||||
refreshAudioContext () {
|
||||
const newAudioContext = new AudioContext()
|
||||
this.audioContext.close()
|
||||
useAudioStore().audioContext = newAudioContext
|
||||
this.audioContext = useAudioStore().getContext()
|
||||
},
|
||||
fadeOutGains () {
|
||||
// useNuxtApp().$logger.log('Fade OUT Gains')
|
||||
// Define the duration of the fade out
|
||||
const fadeDuration = 2.0 // 2 seconds for fade out
|
||||
const currentTime = this.audioContext.currentTime
|
||||
const fadeEndTime = currentTime + fadeDuration
|
||||
|
||||
this.fading = true
|
||||
|
||||
if (this.createdNodes.noiseGain) {
|
||||
// Cancel scheduled values to clear any previous scheduled changes
|
||||
this.createdNodes.noiseGain.gain.cancelScheduledValues(currentTime)
|
||||
// Set the current value
|
||||
this.createdNodes.noiseGain.gain.setValueAtTime(this.createdNodes.noiseGain.gain.value, currentTime)
|
||||
// Schedule the fade out
|
||||
this.createdNodes.noiseGain.gain.linearRampToValueAtTime(0, fadeEndTime)
|
||||
this.noiseGain = 0
|
||||
}
|
||||
|
||||
if (this.createdNodes.musicGain) {
|
||||
// Cancel scheduled values to clear any previous scheduled changes
|
||||
this.createdNodes.musicGain.gain.cancelScheduledValues(currentTime)
|
||||
// Set the current value
|
||||
this.createdNodes.musicGain.gain.setValueAtTime(this.createdNodes.musicGain.gain.value, currentTime)
|
||||
// Schedule the fade out
|
||||
this.createdNodes.musicGain.gain.linearRampToValueAtTime(0, fadeEndTime)
|
||||
this.playing = false
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.fading = false
|
||||
}, fadeDuration * 1000)
|
||||
},
|
||||
fadeInGains () {
|
||||
// useNuxtApp().$logger.log('Fade In Gains')
|
||||
const fadeTime = this.audioContext.currentTime + 6.0
|
||||
this.fading = true
|
||||
const noiseGain = this.createdNodes.noiseGain
|
||||
const musicGain = this.createdNodes.musicGain
|
||||
this.createdNodes.noiseSource.muted = false
|
||||
this.createdNodes.musicSource.muted = false
|
||||
noiseGain.gain.linearRampToValueAtTime(1.0, fadeTime)
|
||||
musicGain.gain.linearRampToValueAtTime(1.0, fadeTime)
|
||||
this.noiseGain = noiseGain.gain.value
|
||||
this.musicGain = musicGain.gain.value
|
||||
this.playing = true
|
||||
setTimeout(() => {
|
||||
this.fading = false
|
||||
}, fadeTime * 1000)
|
||||
},
|
||||
|
||||
handlePlayingUpdate (state: boolean) {
|
||||
// useNuxtApp().$logger.log(`Audio Element is ${state ? 'Playing' : 'Not Playing'}`)
|
||||
if (!this.musicReady && !this.noiseReady) {
|
||||
// useNuxtApp().$logger.log('not yet ready' + this.musicReady + ' ready noise' + this.noiseReady)
|
||||
return
|
||||
}
|
||||
if (state && useAudioStore().isPlaying()) {
|
||||
// Everytime a AudioTag starts playing it is muted, because we want to control everything in web audio
|
||||
// Music has just started react on it.
|
||||
const noiseElement = this.$refs.Noise as typeof AudioElement
|
||||
const noiseAudioElement = noiseElement.$refs.audioElement as HTMLMediaElement
|
||||
const musicElement = this.$refs.Music as typeof AudioElement
|
||||
const musicAudioElement = musicElement.$refs.audioElement as HTMLMediaElement
|
||||
const audioContext = this.audioContext
|
||||
const destination = this.audioContext.destination
|
||||
|
||||
this.createdNodes.musicGain ||= audioContext.createGain()
|
||||
this.createdNodes.noiseGain ||= audioContext.createGain()
|
||||
|
||||
this.createdNodes.musicGain.gain.setValueAtTime(0, audioContext.currentTime)
|
||||
this.createdNodes.noiseGain.gain.setValueAtTime(0, audioContext.currentTime)
|
||||
|
||||
musicAudioElement.muted = false
|
||||
noiseAudioElement.muted = false
|
||||
|
||||
musicAudioElement.volume = 1.0
|
||||
noiseAudioElement.volume = 1.0
|
||||
|
||||
this.createdNodes.noiseSource = audioContext.createMediaElementSource(noiseAudioElement)
|
||||
this.createdNodes.musicSource = audioContext.createMediaElementSource(musicAudioElement)
|
||||
|
||||
this.createdNodes.noiseSource.connect(this.createdNodes.noiseGain)
|
||||
this.createdNodes.musicSource.connect(this.createdNodes.musicGain)
|
||||
|
||||
this.createdNodes.noiseGain.connect(destination)
|
||||
this.createdNodes.musicGain.connect(destination)
|
||||
|
||||
this.fadeInGains()
|
||||
} else {
|
||||
if (this.fading && this.createdNodes.noiseGain && this.createdNodes.musicGain) {
|
||||
this.createdNodes.noiseGain.gain.cancelScheduledValues(this.createdNodes.noiseGain.currentTime)
|
||||
this.createdNodes.musicGain.gain.cancelScheduledValues(this.createdNodes.musicGain.currentTime)
|
||||
}
|
||||
// Music has just stopped react on it.
|
||||
|
||||
setTimeout(() => {
|
||||
this.playing = false
|
||||
this.createdNodes = []
|
||||
this.refreshAudioContext()
|
||||
}, 2000)
|
||||
}
|
||||
},
|
||||
updateNoiseGain (volume: number) {
|
||||
if (this.createdNodes.noiseGain) {
|
||||
this.createdNodes.noiseGain.gain.linearRampToValueAtTime(volume, this.createdNodes.noiseGain.context.currentTime + 0.30)
|
||||
this.noiseGain = this.createdNodes.noiseGain.gain.value
|
||||
}
|
||||
},
|
||||
updateMusicGain (volume: number) {
|
||||
if (this.createdNodes.musicGain) {
|
||||
this.createdNodes.musicGain.gain.linearRampToValueAtTime(volume, this.createdNodes.musicGain.context.currentTime + 0.30)
|
||||
this.musicGain = this.createdNodes.musicGain.gain.value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
73
components/experiments/tests/NoiseMusicWebAudio.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<h1>Test Version NoiseMusicWebAudio: mit WebAudio, ohne Gain, ohne Noise-Patch & ohne Music-Patch</h1>
|
||||
<AudioElement
|
||||
ref="Noise"
|
||||
key="5"
|
||||
:src="noise_src"
|
||||
title="Noise"
|
||||
>
|
||||
<template #default="{}">
|
||||
<div class="icon">
|
||||
<img style="width: 25px" src="~/assets/image/noiseicon.svg">
|
||||
</div>
|
||||
</template>
|
||||
</AudioElement>
|
||||
<AudioElement
|
||||
ref="Music"
|
||||
key="1"
|
||||
:src="forest_src"
|
||||
title="Forest"
|
||||
@update:playing="handlePlayingUpdate"
|
||||
>
|
||||
<template #default="{ }">
|
||||
<div class="icon">
|
||||
<!-- tropic icon -->
|
||||
<img style="width: 25px" src="~/assets/image/musicicon.svg">
|
||||
</div>
|
||||
</template>
|
||||
<!-- Slot content for AudioElement, if needed -->
|
||||
</AudioElement>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import AudioElement from '../AudioElement.vue'
|
||||
import { useAudioStore } from '../../../stores/audio'
|
||||
|
||||
export default {
|
||||
name: 'NoiseMusicGain',
|
||||
components: { AudioElement },
|
||||
data () {
|
||||
return {
|
||||
audioContext: useAudioStore().getContext(),
|
||||
createdNodes: {} as any,
|
||||
playing: false,
|
||||
paused: false,
|
||||
noise_src: window.location.origin + useRuntimeConfig().public.noise_src,
|
||||
forest_src: window.location.origin + useRuntimeConfig().public.tracks.forest_src
|
||||
}
|
||||
},
|
||||
beforeUnmount () {
|
||||
this.audioContext.close()
|
||||
this.audioContext = null
|
||||
},
|
||||
methods: {
|
||||
handlePlayingUpdate () {
|
||||
const noiseElement = this.$refs.Noise as typeof AudioElement
|
||||
const noiseAudioElement = noiseElement.$refs.audioElement as HTMLMediaElement
|
||||
const musicElement = this.$refs.Music as typeof AudioElement
|
||||
const musicAudioElement = musicElement.$refs.audioElement as HTMLMediaElement
|
||||
const audioContext = this.audioContext
|
||||
const destination = this.audioContext.destination
|
||||
|
||||
this.createdNodes.noiseSource ||= audioContext.createMediaElementSource(noiseAudioElement)
|
||||
this.createdNodes.musicSource ||= audioContext.createMediaElementSource(musicAudioElement)
|
||||
|
||||
musicAudioElement.muted = false
|
||||
noiseAudioElement.muted = false
|
||||
|
||||
this.createdNodes.noiseSource.connect(destination)
|
||||
this.createdNodes.musicSource.connect(destination)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
354
components/experiments/tests/showcases/PlayerComponent.vue
Normal file
@@ -0,0 +1,354 @@
|
||||
<template>
|
||||
<div>
|
||||
<h4>{{ currentScene.file }}</h4>
|
||||
<h4>Node {{ currentScene.node ? true : false }}</h4>
|
||||
<h4>{{ currentScene.title }}</h4>
|
||||
<h4>Howl {{ currentScene.howl ? true : false }}</h4>
|
||||
</div>
|
||||
<div class="rnboplayer">
|
||||
<button class="play yellow" @click="play" />
|
||||
<button v-if="playing" class="pause yellow" @click="pause" />
|
||||
<button v-if="playing" @click="useNuxtApp().$logger.log('hit play')">
|
||||
Pause
|
||||
</button>
|
||||
<div class="row">
|
||||
<div class="slider">
|
||||
<div class="icon">
|
||||
<!-- tropic icon -->
|
||||
<img style="width: 25px" src="~/assets/image/noiseicon.svg">
|
||||
</div>
|
||||
<input
|
||||
id="gain-control"
|
||||
v-model="outputNoiseGain"
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
step="1"
|
||||
@wheel="changeNoiseGain"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="slider">
|
||||
<div class="icon">
|
||||
<!-- tropic icon -->
|
||||
<img style="width: 25px" src="~/assets/image/musicicon.svg">
|
||||
</div>
|
||||
<input
|
||||
id="gain-control"
|
||||
v-model="outputMusicGain"
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
default="50"
|
||||
step="1"
|
||||
@wheel="changeMusicGain"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onBeforeUnmount, ref, computed, watch } from 'vue'
|
||||
import Player from '~/components/Player/Player.js'
|
||||
import setupNodes from '~/components/Player/Nodes'
|
||||
import { useUserStore } from '~/stores/user.js'
|
||||
import { usePlayerStore } from '~/stores/player.js'
|
||||
import tracksConfig from '~/tracks.config'
|
||||
|
||||
// Stores
|
||||
const userStore = useUserStore()
|
||||
const playerStore = usePlayerStore()
|
||||
|
||||
const playing = computed(() => usePlayerStore().playing as boolean)
|
||||
|
||||
// Refs
|
||||
const scenePlayer = ref<Player | null>(null)
|
||||
const noisePlayer = ref<Player | null>(null)
|
||||
const playlistIndex = ref(scenePlayer.value?.index || 0)
|
||||
|
||||
// Sliders
|
||||
const outputMusicGain = ref(0)
|
||||
const outputNoiseGain = ref(0)
|
||||
|
||||
// Playlists
|
||||
const scenesPlaylist = ref([
|
||||
{ title: 'Lagoon', file: 'lagoon', howl: null, node: null },
|
||||
{ title: 'Meadow', file: 'meadow', howl: null, node: null },
|
||||
{ title: 'Tropics', file: 'tropics', howl: null, node: null },
|
||||
{ title: 'Forest', file: 'forest', howl: null, node: null }
|
||||
])
|
||||
const howlsReadyCount = computed(() =>
|
||||
scenesPlaylist.value.filter(item => item.howl !== null).length
|
||||
)
|
||||
|
||||
const nodesReadyCount = computed(() =>
|
||||
scenesPlaylist.value.filter(item => item.node !== null).length
|
||||
)
|
||||
|
||||
const noisePlaylist = ref([
|
||||
{ title: 'noise', file: 'noise', howl: null, node: null }
|
||||
])
|
||||
const currentScene = computed(() => scenesPlaylist.value[0])
|
||||
|
||||
// Gain Nodes
|
||||
const noiseGainNode = ref<GainNode | null>(null)
|
||||
const musicGainNode = ref<GainNode | null>(null)
|
||||
|
||||
function play () {
|
||||
playerStore.playing = true
|
||||
useNuxtApp().$logger.log('current playing = ', playerStore.playing)
|
||||
}
|
||||
function pause () {
|
||||
playerStore.playing = false
|
||||
useNuxtApp().$logger.log('current playing = ', playerStore.playing)
|
||||
}
|
||||
|
||||
function changeNoiseGain (event: WheelEvent) {
|
||||
event.preventDefault()
|
||||
const delta = Math.sign(event.deltaY)
|
||||
if (noiseGainNode.value && isWithinRange(outputNoiseGain.value - delta)) {
|
||||
outputNoiseGain.value -= delta
|
||||
}
|
||||
}
|
||||
|
||||
function changeMusicGain (event: WheelEvent) {
|
||||
event.preventDefault()
|
||||
const delta = Math.sign(event.deltaY)
|
||||
if (musicGainNode.value && isWithinRange(outputMusicGain.value - delta)) {
|
||||
scenePlayer.value?.setVolume()
|
||||
outputMusicGain.value -= delta
|
||||
}
|
||||
}
|
||||
|
||||
function isWithinRange (val: number) {
|
||||
return val >= 0 && val <= 100
|
||||
}
|
||||
|
||||
// Lifecycle
|
||||
onMounted(async () => {
|
||||
const logger = useNuxtApp().$logger
|
||||
|
||||
scenePlayer.value = new Player(scenesPlaylist, 'sounds')
|
||||
noisePlayer.value = new Player(noisePlaylist, 'masking')
|
||||
|
||||
scenePlayer.value.initializeHowl(scenePlayer.value.index)
|
||||
noisePlayer.value.initializeHowl(noisePlayer.value.index)
|
||||
|
||||
const userScenery = userStore.getUserScenery
|
||||
const filteredScenes = scenePlayer.value.playlist.filter(
|
||||
(playItem: { title: string }) => playItem.title.toLowerCase() === userScenery.toLowerCase()
|
||||
)
|
||||
|
||||
const noiseNode = noisePlayer.value.playlist[0].node
|
||||
const response = await setupNodes(noiseNode, filteredScenes[0].node)
|
||||
|
||||
if (response === null) {
|
||||
logger.info('Got no gain nodes from setupNodes, continue with HTML5')
|
||||
// noisePlayer.value.playlist[0].howl.play()
|
||||
// scenePlayer.value.playlist[0].howl.play()
|
||||
} else {
|
||||
try {
|
||||
// filteredScenes[0].howl.play()
|
||||
// noisePlayer.value.playlist[0].howl.play()
|
||||
if (response.length > 1) {
|
||||
noiseGainNode.value = response[0]
|
||||
musicGainNode.value = response[1]
|
||||
}
|
||||
if (musicGainNode.value && noiseGainNode.value) {
|
||||
musicGainNode.value.gain.value = 0.5
|
||||
noiseGainNode.value.gain.value = 0.5
|
||||
}
|
||||
} catch (error) {
|
||||
useNuxtApp().$logger.error(error)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
noisePlayer.value = null
|
||||
scenePlayer.value = null
|
||||
playerStore.resetAudioContext()
|
||||
})
|
||||
|
||||
// Watches
|
||||
watch(playlistIndex, (val) => {
|
||||
const player = scenePlayer.value as Player
|
||||
player.index = val
|
||||
// musicGainNode.value?.gain.linearRampToValueAtTime(val / 100, musicGainNode.value.context.currentTime + 0.2)
|
||||
})
|
||||
watch(outputMusicGain, (val) => {
|
||||
musicGainNode.value?.gain.linearRampToValueAtTime(val / 100, musicGainNode.value.context.currentTime + 0.2)
|
||||
})
|
||||
watch(outputNoiseGain, (val) => {
|
||||
noiseGainNode.value?.gain.linearRampToValueAtTime(val / 100, noiseGainNode.value.context.currentTime + 0.2)
|
||||
})
|
||||
watch(
|
||||
() => noisePlayer.value?.playlist[0]?.node,
|
||||
(node) => {
|
||||
if (node) { useNuxtApp().$logger.log('🎧 Node ist jetzt da:', node) }
|
||||
}
|
||||
)
|
||||
watch(
|
||||
() => playerStore.playing,
|
||||
(state) => {
|
||||
if (state && noisePlayer && scenePlayer) {
|
||||
noisePlayer.value?.play()
|
||||
scenePlayer.value?.play()
|
||||
useNuxtApp().$logger.log('🎧 Player spielt')
|
||||
} else {
|
||||
noisePlayer.value?.pause()
|
||||
scenePlayer.value?.pause()
|
||||
useNuxtApp().$logger.log('🎧 Player spielt nicht')
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.rnboplayer{
|
||||
position: fixed;
|
||||
width: 220px; /* Or specify a fixed width like 220px if you prefer */
|
||||
max-width: 220px; /* This line might be redundant depending on your width strategy */
|
||||
height: 100px;
|
||||
display: inline-grid;
|
||||
z-index: 2;
|
||||
bottom: 11%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.player {
|
||||
background-color: #fff;
|
||||
border-radius: 12px;
|
||||
position:sticky;
|
||||
width: 225px;
|
||||
height: inherit;
|
||||
display:flex;
|
||||
float: bottom;
|
||||
}
|
||||
.player button {
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px; /* Spacing between items */
|
||||
width: 225px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.icon, .slider {
|
||||
flex: 1 1 100px; /* Flex-grow, flex-shrink, flex-basis */
|
||||
display: flex;
|
||||
align-items: center; /* Center items vertically */
|
||||
justify-content: center; /* Center items horizontally */
|
||||
}
|
||||
.icon {
|
||||
/* Add padding around the icon for margin */
|
||||
margin-right: 15px; /* Adjust this value as needed */
|
||||
|
||||
/* Align items if using flexbox */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.icon img {
|
||||
/* Adjust width and height as needed or keep them auto to maintain aspect ratio */
|
||||
width: auto;
|
||||
height: 100%; /* Example height, adjust based on your icon size */
|
||||
}
|
||||
|
||||
.slider input[type=range] {
|
||||
width: 100%; /* Full width of its parent */
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
@media (min-width: 600px) {
|
||||
.row {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.icon, .slider {
|
||||
flex: 1; /* Take up equal space */
|
||||
}
|
||||
}
|
||||
|
||||
/* Styles the track */
|
||||
input[type="range"]::-webkit-slider-runnable-track {
|
||||
background: #e9c046; /* yellow track */
|
||||
height: 8px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
input[type="range"]::-moz-range-track {
|
||||
background: #e9c046; /* yellow track */
|
||||
height: 8px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
input[type="range"]::-ms-track {
|
||||
background: #e9c046; /* yellow track */
|
||||
border-color: transparent;
|
||||
color: transparent;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.play.yellow {
|
||||
background: rgba(255, 176, 66, 0.008);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 0 0 rgba(255, 177, 66, 1);
|
||||
animation: pulse-yellow 4s infinite;
|
||||
position: fixed;
|
||||
bottom: 40%;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
background-image: url('/images/playbtn.svg');
|
||||
background-repeat:no-repeat;
|
||||
background-attachment:fixed;
|
||||
background-position: 58% 55%;
|
||||
}
|
||||
.pause.yellow {
|
||||
background: rgba(255, 176, 66, 0.008);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 0 0 rgba(255, 177, 66, 1);
|
||||
opacity: 0.05;
|
||||
position: fixed;
|
||||
bottom: 40%;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
background-image: url('/images/pausebtn.svg');
|
||||
background-size: 130px 100px;
|
||||
background-repeat:no-repeat;
|
||||
background-attachment:fixed;
|
||||
background-position: center;
|
||||
}
|
||||
.pause.yellow:hover{
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@keyframes pulse-yellow {
|
||||
0% {
|
||||
transform: scale(0.95);
|
||||
box-shadow: 0 0 0 0 rgba(255, 177, 66, 0.7);
|
||||
}
|
||||
|
||||
70% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 0 0 10px rgba(255, 177, 66, 0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(0.95);
|
||||
box-shadow: 0 0 0 0 rgba(255, 177, 66, 0);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
517
components/homebar.vue
Normal file
235
components/sliders/MusicGainSlider.vue
Normal file
@@ -0,0 +1,235 @@
|
||||
<template>
|
||||
<div class="slider-wrapper">
|
||||
<!-- Slot for passing any audio element -->
|
||||
<img
|
||||
v-if="volumeValue == 0"
|
||||
style="width: 25px; height: 25px;"
|
||||
src="~/assets/image/music_muted.svg"
|
||||
:title="t('unmuteSlider')"
|
||||
@click="toggleMute()"
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
style="width: 25px; height: 25px;"
|
||||
src="~/assets/image/music.svg"
|
||||
:title="t('muteSlider')"
|
||||
@click="toggleMute()"
|
||||
>
|
||||
<div class="slider">
|
||||
<input
|
||||
id="gain-control-music"
|
||||
ref="MusicSlider"
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.001"
|
||||
data-toggle="tooltip"
|
||||
data-placement="top"
|
||||
:title="t('textSlider')"
|
||||
@input="changeVolumeOnTrack"
|
||||
@wheel.prevent="changeVolumeOnWheel"
|
||||
>
|
||||
<span
|
||||
class="slider-progress-bar"
|
||||
:style="{ width: `${volumeValue * 100}%` }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { useAudioStore } from '~/stores/audio'
|
||||
|
||||
const MusicSlider = ref(null)
|
||||
const audioStore = useAudioStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
// Computed volume value from the audio store
|
||||
const volumeValue = computed(() => audioStore.getVolume)
|
||||
// Stores volume before muting
|
||||
let volumeOnMute = audioStore.getVolume
|
||||
|
||||
/**
|
||||
* Toggles mute state:
|
||||
* - If volume > 0: saves current volume and sets to 0
|
||||
* - If volume == 0: restores saved volume
|
||||
*/
|
||||
const toggleMute = () => {
|
||||
if (audioStore.getVolume !== 0) {
|
||||
volumeOnMute = audioStore.getVolume
|
||||
audioStore.setVolume(0)
|
||||
} else {
|
||||
audioStore.setVolume(volumeOnMute)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncs external slider DOM element with the reactive volume state
|
||||
*/
|
||||
watch(volumeValue, (newValue) => {
|
||||
if (MusicSlider.value) {
|
||||
const inputSlider = MusicSlider.value as HTMLInputElement
|
||||
inputSlider.valueAsNumber = newValue
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Handles scroll wheel changes over the slider element to increment/decrement volume.
|
||||
* Clamped between 0 and 1 with 0.02 steps.
|
||||
*
|
||||
* @param {WheelEvent} event - Scroll event on the volume slider
|
||||
*/
|
||||
const changeVolumeOnWheel = (event: WheelEvent) => {
|
||||
const deltaY = event.deltaY
|
||||
if (deltaY < 0) {
|
||||
const volumeAdd = Math.min(1, volumeValue.value + 0.02)
|
||||
if (volumeAdd <= 1) { audioStore.setVolume(volumeAdd) }
|
||||
} else {
|
||||
const volumeCut = Math.max(0, volumeValue.value - 0.02)
|
||||
if (volumeCut >= 0) { audioStore.setVolume(volumeCut) }
|
||||
}
|
||||
audioStore.setVolume(volumeValue.value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets volume when user moves the range input (slider)
|
||||
*
|
||||
* @param {InputEvent} event - Input change from range slider
|
||||
*/
|
||||
const changeVolumeOnTrack = (event: InputEvent) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
const volume = Number(target.value)
|
||||
if (volume >= 0 && volume <= 1) {
|
||||
audioStore.setVolume(volume)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.slider-wrapper{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin: 1em auto;
|
||||
}
|
||||
|
||||
.slider-icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 576px) {
|
||||
.slider{
|
||||
max-width: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
.slider-progress-bar {
|
||||
position: absolute;
|
||||
height: 10px;
|
||||
background-color: #e9c046;
|
||||
border-top-left-radius: 5px;
|
||||
border-bottom-left-radius: 5px;
|
||||
border-radius: 5px;
|
||||
left: 0;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* Allgemeine Einstellungen für den Slider */
|
||||
input[type="range"] {
|
||||
-webkit-appearance: none; /* Entfernt das Standard-Styling in Webkit-Browsern */
|
||||
appearance: none; /* Entfernt Standard-Styling in anderen Browsern */
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
background: #fff;
|
||||
border-radius: 5px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
z-index:1;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
/* Styling für den Track in Webkit-Browsern */
|
||||
input[type="range"]::-webkit-slider-runnable-track {
|
||||
height: 10px;
|
||||
background: transparent;
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* Styling für den Thumb in Webkit-Browsern */
|
||||
input[type="range"]::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
margin-top: -5px;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
border: 2px solid #e9c046;
|
||||
}
|
||||
|
||||
/* Styling für den Track in Firefox */
|
||||
input[type="range"]::-moz-range-track {
|
||||
height: 10px;
|
||||
background: transparent;
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Styling für den Thumb in Firefox */
|
||||
input[type="range"]::-moz-range-thumb {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
background-color: #e9c046;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
border: 2px solid #e9c046;
|
||||
margin-top: -5px;
|
||||
}
|
||||
|
||||
input[type="range"]::-moz-range-progress {
|
||||
height: 10px;
|
||||
background-color: #e9c046;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
/* Styling für den Track in Internet Explorer/Edge */
|
||||
input[type="range"]::-ms-track {
|
||||
height: 10px;
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
color: transparent;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
/* Styling für den Thumb in Internet Explorer/Edge */
|
||||
input[type="range"]::-ms-thumb {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
background: #f5f5f5;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 3px rgba(0, 0, 0, 0.3);
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
border: 2px solid #e9c046;
|
||||
}
|
||||
|
||||
/* Entfernt Ticks in IE */
|
||||
input[type="range"]::-ms-ticks-after,
|
||||
input[type="range"]::-ms-ticks-before {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
235
components/sliders/NoiseGainSlider.vue
Normal file
@@ -0,0 +1,235 @@
|
||||
<template>
|
||||
<div class="slider-wrapper">
|
||||
<!-- Slot for passing any audio element -->
|
||||
<img
|
||||
v-if="volumeValue == 0"
|
||||
style="width: 25px; height: 25px;"
|
||||
src="~/assets/image/sound_muted.svg"
|
||||
:title="t('unmuteSlider')"
|
||||
@click="toggleMute()"
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
style="width: 25px; height: 25px;"
|
||||
src="~/assets/image/sound.svg"
|
||||
:title="t('muteSlider')"
|
||||
@click="toggleMute()"
|
||||
>
|
||||
<div class="slider">
|
||||
<input
|
||||
id="gain-control-music"
|
||||
ref="NoiseSlider"
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.001"
|
||||
data-toggle="tooltip"
|
||||
data-placement="top"
|
||||
:title="t('textSlider')"
|
||||
@input="changeVolumeOnTrack"
|
||||
@wheel.prevent="changeVolumeOnWheel"
|
||||
>
|
||||
<span
|
||||
class="slider-progress-bar"
|
||||
:style="{ width: `${volumeValue * 100}%` }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { useAudioStore } from '~/stores/audio'
|
||||
|
||||
const NoiseSlider = ref(null)
|
||||
const audioStore = useAudioStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
// Computed volume value from the audio store
|
||||
const volumeValue = computed(() => audioStore.getNoiseVolume)
|
||||
// Stores volume before muting
|
||||
let volumeOnMute = audioStore.getNoiseVolume
|
||||
|
||||
/**
|
||||
* Toggles mute state:
|
||||
* - If volume > 0: saves current volume and sets to 0
|
||||
* - If volume == 0: restores saved volume
|
||||
*/
|
||||
const toggleMute = () => {
|
||||
if (audioStore.getNoiseVolume !== 0) {
|
||||
volumeOnMute = audioStore.getNoiseVolume
|
||||
audioStore.setNoiseVolume(0)
|
||||
} else {
|
||||
audioStore.setNoiseVolume(volumeOnMute)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncs external slider DOM element with the reactive volume state
|
||||
*/
|
||||
watch(volumeValue, (newValue) => {
|
||||
if (NoiseSlider.value) {
|
||||
const inputSlider = NoiseSlider.value as HTMLInputElement
|
||||
inputSlider.valueAsNumber = newValue
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Handles scroll wheel changes over the slider element to increment/decrement volume.
|
||||
* Clamped between 0 and 1 with 0.02 steps.
|
||||
*
|
||||
* @param {WheelEvent} event - Scroll event on the volume slider
|
||||
*/
|
||||
const changeVolumeOnWheel = (event: WheelEvent) => {
|
||||
const deltaY = event.deltaY
|
||||
if (deltaY < 0) {
|
||||
const volumeAdd = Math.min(1, volumeValue.value + 0.02)
|
||||
if (volumeAdd <= 1) { audioStore.setNoiseVolume(volumeAdd) }
|
||||
} else {
|
||||
const volumeCut = Math.max(0, volumeValue.value - 0.02)
|
||||
if (volumeCut >= 0) { audioStore.setNoiseVolume(volumeCut) }
|
||||
}
|
||||
audioStore.setNoiseVolume(volumeValue.value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets volume when user moves the range input (slider)
|
||||
*
|
||||
* @param {InputEvent} event - Input change from range slider
|
||||
*/
|
||||
const changeVolumeOnTrack = (event: InputEvent) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
const volume = Number(target.value)
|
||||
if (volume >= 0 && volume <= 1) {
|
||||
audioStore.setNoiseVolume(volume)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.slider-wrapper{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin: 1em auto;
|
||||
}
|
||||
|
||||
.slider-icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 576px) {
|
||||
.slider{
|
||||
max-width: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
.slider-progress-bar {
|
||||
position: absolute;
|
||||
height: 10px;
|
||||
background-color: #e9c046;
|
||||
border-top-left-radius: 5px;
|
||||
border-bottom-left-radius: 5px;
|
||||
border-radius: 5px;
|
||||
left: 0;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* Allgemeine Einstellungen für den Slider */
|
||||
input[type="range"] {
|
||||
-webkit-appearance: none; /* Entfernt das Standard-Styling in Webkit-Browsern */
|
||||
appearance: none; /* Entfernt Standard-Styling in anderen Browsern */
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
background: #fff;
|
||||
border-radius: 5px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
z-index:1;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
/* Styling für den Track in Webkit-Browsern */
|
||||
input[type="range"]::-webkit-slider-runnable-track {
|
||||
height: 10px;
|
||||
background: transparent;
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* Styling für den Thumb in Webkit-Browsern */
|
||||
input[type="range"]::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
margin-top: -5px;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
border: 2px solid #e9c046;
|
||||
}
|
||||
|
||||
/* Styling für den Track in Firefox */
|
||||
input[type="range"]::-moz-range-track {
|
||||
height: 10px;
|
||||
background: transparent;
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Styling für den Thumb in Firefox */
|
||||
input[type="range"]::-moz-range-thumb {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
background-color: #e9c046;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
border: 2px solid #e9c046;
|
||||
margin-top: -5px;
|
||||
}
|
||||
|
||||
input[type="range"]::-moz-range-progress {
|
||||
height: 10px;
|
||||
background-color: #e9c046;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
/* Styling für den Track in Internet Explorer/Edge */
|
||||
input[type="range"]::-ms-track {
|
||||
height: 10px;
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
color: transparent;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
/* Styling für den Thumb in Internet Explorer/Edge */
|
||||
input[type="range"]::-ms-thumb {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
background: #f5f5f5;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 3px rgba(0, 0, 0, 0.3);
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
border: 2px solid #e9c046;
|
||||
}
|
||||
|
||||
/* Entfernt Ticks in IE */
|
||||
input[type="range"]::-ms-ticks-after,
|
||||
input[type="range"]::-ms-ticks-before {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
63
components/subscription/PriceCalculator.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div>
|
||||
<label for="seats">Anzahl der Sitze:</label>
|
||||
<input id="seats" v-model.number="seats" type="number" min="1" @input="updatePrice">
|
||||
<p>Preis: {{ price }} €</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
setup () {
|
||||
const seats = ref(1) // Default to 1 seat
|
||||
const price = ref(0)
|
||||
|
||||
function calculatePrice (seats: number): number {
|
||||
const basePricePerSeat = 112
|
||||
const maxDiscount = 0.4916 // 49.16% discount at 500 seats
|
||||
const maxSeats = 500
|
||||
|
||||
// Calculate the discount factor based on the number of seats (linear)
|
||||
let discountFactor = maxDiscount * (seats / maxSeats)
|
||||
|
||||
// Ensure the discount doesn't exceed the maximum discount
|
||||
if (discountFactor > maxDiscount) {
|
||||
discountFactor = maxDiscount
|
||||
}
|
||||
|
||||
// Calculate the price with the linear discount
|
||||
const totalPrice = basePricePerSeat * seats * (1 - discountFactor)
|
||||
|
||||
// Round the result to 2 decimal places
|
||||
return Math.round(totalPrice * 100) / 100
|
||||
}
|
||||
|
||||
// Update the price whenever the seat count changes
|
||||
function updatePrice () {
|
||||
price.value = calculatePrice(seats.value)
|
||||
}
|
||||
|
||||
return {
|
||||
seats,
|
||||
price,
|
||||
updatePrice
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Optional: some simple styling */
|
||||
input {
|
||||
margin: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.2rem;
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
5
components/svg/bootombar/clip0_143_324.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<g clip-path="url(#clip0_143_324)">
|
||||
<path d="M5.16262 2.38755C5.14543 2.39067 5.12825 2.39536 5.11262 2.40005C4.92668 2.44224 4.79543 2.60942 4.80012 2.80005V17.2C4.79856 17.3438 4.87356 17.4782 4.99856 17.5516C5.12356 17.6235 5.27668 17.6235 5.40168 17.5516C5.52668 17.4782 5.60168 17.3438 5.60012 17.2V2.80005C5.60481 2.68442 5.55793 2.57349 5.47512 2.49536C5.39075 2.41567 5.27668 2.37661 5.16262 2.38755ZM14.7626 2.38755C14.7454 2.39067 14.7282 2.39536 14.7126 2.40005C14.5267 2.44224 14.3954 2.60942 14.4001 2.80005V17.2C14.3986 17.3438 14.4736 17.4782 14.5986 17.5516C14.7236 17.6235 14.8767 17.6235 15.0017 17.5516C15.1267 17.4782 15.2017 17.3438 15.2001 17.2V2.80005C15.2048 2.68442 15.1579 2.57349 15.0751 2.49536C14.9907 2.41567 14.8767 2.37661 14.7626 2.38755ZM2.76262 5.98755C2.74543 5.99067 2.72825 5.99536 2.71262 6.00005C2.52668 6.04224 2.39543 6.20942 2.40012 6.40005V13.6C2.39856 13.7438 2.47356 13.8782 2.59856 13.9516C2.72356 14.0235 2.87668 14.0235 3.00168 13.9516C3.12668 13.8782 3.20168 13.7438 3.20012 13.6V6.40005C3.20481 6.28442 3.15793 6.17349 3.07512 6.09536C2.99075 6.01567 2.87668 5.97661 2.76262 5.98755ZM17.1626 5.98755C17.1454 5.99067 17.1282 5.99536 17.1126 6.00005C16.9267 6.04224 16.7954 6.20942 16.8001 6.40005V13.6C16.7986 13.7438 16.8736 13.8782 16.9986 13.9516C17.1236 14.0235 17.2767 14.0235 17.4017 13.9516C17.5267 13.8782 17.6017 13.7438 17.6001 13.6V6.40005C17.6048 6.28442 17.5579 6.17349 17.4751 6.09536C17.3907 6.01567 17.2767 5.97661 17.1626 5.98755ZM7.56262 6.38755C7.54543 6.39067 7.52825 6.39536 7.51262 6.40005C7.32668 6.44224 7.19543 6.60942 7.20012 6.80005V10V13.2C7.19856 13.3438 7.27356 13.4782 7.39856 13.5516C7.52356 13.6235 7.67668 13.6235 7.80168 13.5516C7.92668 13.4782 8.00168 13.3438 8.00012 13.2V6.80005C8.00481 6.68442 7.95793 6.57349 7.87512 6.49536C7.79075 6.41567 7.67668 6.37661 7.56262 6.38755ZM12.3626 6.38755C12.3454 6.39067 12.3282 6.39536 12.3126 6.40005C12.1267 6.44224 11.9954 6.60942 12.0001 6.80005V13.2C11.9986 13.3438 12.0736 13.4782 12.1986 13.5516C12.3236 13.6235 12.4767 13.6235 12.6017 13.5516C12.7267 13.4782 12.8017 13.3438 12.8001 13.2V6.80005C12.8048 6.68442 12.7579 6.57349 12.6751 6.49536C12.5907 6.41567 12.4767 6.37661 12.3626 6.38755ZM0.362622 8.38755C0.345434 8.39067 0.328247 8.39536 0.312622 8.40005C0.126684 8.44224 -0.00456587 8.60942 0.000121649 8.80005V11.2C-0.00144085 11.3438 0.0735592 11.4782 0.198559 11.5516C0.323559 11.6235 0.476684 11.6235 0.601684 11.5516C0.726684 11.4782 0.801684 11.3438 0.800122 11.2V8.80005C0.804809 8.68442 0.757934 8.57349 0.675122 8.49536C0.590747 8.41567 0.476684 8.37661 0.362622 8.38755ZM9.96262 8.38755C9.94543 8.39067 9.92825 8.39536 9.91262 8.40005C9.72668 8.44224 9.59543 8.60942 9.60012 8.80005V11.2C9.59856 11.3438 9.67356 11.4782 9.79856 11.5516C9.92356 11.6235 10.0767 11.6235 10.2017 11.5516C10.3267 11.4782 10.4017 11.3438 10.4001 11.2V8.80005C10.4048 8.68442 10.3579 8.57349 10.2751 8.49536C10.1907 8.41567 10.0767 8.37661 9.96262 8.38755ZM19.5626 8.38755C19.5454 8.39067 19.5282 8.39536 19.5126 8.40005C19.3267 8.44224 19.1954 8.60942 19.2001 8.80005V11.2C19.1986 11.3438 19.2736 11.4782 19.3986 11.5516C19.5236 11.6235 19.6767 11.6235 19.8017 11.5516C19.9267 11.4782 20.0017 11.3438 20.0001 11.2V8.80005C20.0048 8.68442 19.9579 8.57349 19.8751 8.49536C19.7907 8.41567 19.6767 8.37661 19.5626 8.38755Z" fill="#030504" fill-opacity="0.8"/><rect x="1.82617" y="0.907189" width="24.343" height="1.3" rx="0.65" transform="rotate(45 1.82617 0.907189)" fill="#333534" stroke="white" stroke-width="0.5"/>
|
||||
</g><defs><clipPath id="clip0_143_324"><rect width="20" height="20" fill="white"/></clipPath></defs>
|
||||
</svg>
|
After Width: | Height: | Size: 3.7 KiB |
3
components/svg/bootombar/foucusedIcon.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="24" viewBox="0 0 19 20" fill="none">
|
||||
<path d="M2.69824 0.055674C1.5521 0.101019 0.5 1.0361 0.5 2.29591V17.7041C0.5 19.3839 2.37138 20.486 3.84082 19.6719L17.7451 11.9678C19.2568 11.1301 19.2568 8.86988 17.7451 8.03224L3.84082 0.328135C3.47346 0.124617 3.08029 0.0405589 2.69824 0.055674ZM2.71582 1.53321C2.84512 1.53266 2.98064 1.56618 3.11328 1.63966L17.0186 9.34376C17.5678 9.64811 17.5678 10.3519 17.0186 10.6563L3.11328 18.3604C2.58272 18.6543 2 18.3104 2 17.7041V2.29591C2 1.99278 2.14535 1.75582 2.35742 1.63087C2.46346 1.5684 2.58652 1.53377 2.71582 1.53321Z" fill="#030504" fill-opacity="0.4"/>
|
||||
</svg>
|
After Width: | Height: | Size: 671 B |
3
components/svg/bootombar/unknown1.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="24" viewBox="0 0 20 24" fill="none">
|
||||
<path d="M8.07207 0.952443C7.8077 0.958068 7.59582 1.17557 7.59957 1.43994V14.3999C7.59957 14.9999 7.6802 15.5681 7.98395 16.0349C8.2877 16.5018 8.8502 16.7999 9.51957 16.7999V20.2499C8.96457 20.4506 8.55957 20.9793 8.55957 21.5999C8.55957 22.3893 9.2102 23.0399 9.99957 23.0399C10.7889 23.0399 11.4396 22.3893 11.4396 21.5999C11.4396 20.9793 11.0346 20.4506 10.4796 20.2499V16.7999C11.0964 16.7999 11.6346 16.5018 11.9514 16.0499C12.2702 15.5962 12.3996 15.0206 12.3996 14.3999V1.43994C12.4014 1.26744 12.3114 1.10619 12.1614 1.01807C12.0114 0.931818 11.8277 0.931818 11.6777 1.01807C11.5277 1.10619 11.4377 1.26744 11.4396 1.43994V14.3999C11.4396 14.8818 11.3289 15.2662 11.1677 15.4968C11.0046 15.7293 10.8227 15.8399 10.4796 15.8399H9.51957C9.08645 15.8399 8.93082 15.7293 8.7902 15.5118C8.6477 15.2943 8.55957 14.9024 8.55957 14.3999V1.43994C8.56145 1.31057 8.51082 1.18494 8.41895 1.09307C8.32707 1.00119 8.20145 0.950568 8.07207 0.952443ZM3.0377 3.83619C2.91207 3.83807 2.79394 3.89057 2.7077 3.98057C0.27582 6.41244 0.27582 10.3724 2.7077 12.8043C2.8277 12.9299 3.0077 12.9806 3.17457 12.9374C3.34332 12.8943 3.47457 12.7612 3.5177 12.5943C3.5627 12.4256 3.51207 12.2474 3.38645 12.1274C1.3202 10.0593 1.3202 6.72557 3.38645 4.65932C3.52895 4.52057 3.5702 4.31057 3.49332 4.12869C3.41645 3.94494 3.23644 3.82869 3.0377 3.83619ZM16.9464 4.08182C16.7514 4.08182 16.5771 4.19994 16.5039 4.38182C16.4308 4.56182 16.4739 4.76994 16.6146 4.90494C18.6789 6.97119 18.6789 10.3068 16.6146 12.3731C16.4889 12.4931 16.4383 12.6731 16.4814 12.8399C16.5246 13.0087 16.6577 13.1399 16.8246 13.1831C16.9933 13.2281 17.1714 13.1774 17.2914 13.0518C19.7252 10.6181 19.7252 6.65994 17.2914 4.22619C17.2014 4.13432 17.0777 4.08182 16.9464 4.08182ZM4.3952 5.19182C4.26957 5.19557 4.15144 5.24807 4.0652 5.33807C2.38144 7.02182 2.38144 9.76494 4.0652 11.4468C4.1852 11.5724 4.3652 11.6231 4.53207 11.5799C4.70082 11.5368 4.83207 11.4037 4.8752 11.2368C4.9202 11.0681 4.86957 10.8899 4.74395 10.7699C3.4277 9.45369 3.4277 7.33307 4.74395 6.01682C4.88645 5.87807 4.9277 5.66807 4.85082 5.48619C4.77395 5.30244 4.59395 5.18619 4.3952 5.19182ZM15.5889 5.43932C15.3939 5.43932 15.2196 5.55744 15.1446 5.73932C15.0714 5.91932 15.1146 6.12744 15.2552 6.26244C16.5714 7.57869 16.5714 9.69932 15.2552 11.0156C15.1296 11.1356 15.0789 11.3156 15.1239 11.4824C15.1671 11.6512 15.2983 11.7824 15.4671 11.8256C15.6339 11.8706 15.8139 11.8199 15.9339 11.6943C17.6177 10.0106 17.6177 7.26744 15.9339 5.58557C15.8439 5.49182 15.7202 5.43932 15.5889 5.43932ZM5.7527 6.54932C5.62707 6.55307 5.50895 6.60557 5.4227 6.69557C4.48895 7.62932 4.48895 9.15744 5.4227 10.0893C5.5427 10.2149 5.7227 10.2656 5.88957 10.2224C6.05832 10.1793 6.18957 10.0462 6.2327 9.87932C6.2777 9.71057 6.22707 9.53244 6.10145 9.41244C5.5352 8.84432 5.5352 7.94057 6.10145 7.37432C6.24395 7.23557 6.2852 7.02557 6.20832 6.84369C6.13144 6.65994 5.95145 6.54369 5.7527 6.54932ZM14.2314 6.79682C14.0364 6.79869 13.8621 6.91682 13.7871 7.09682C13.7139 7.27869 13.7571 7.48494 13.8977 7.62182C14.4639 8.18807 14.4639 9.09182 13.8977 9.65807C13.7721 9.77807 13.7214 9.95807 13.7664 10.1249C13.8096 10.2937 13.9408 10.4249 14.1096 10.4681C14.2764 10.5131 14.4564 10.4624 14.5764 10.3368C15.5102 9.40307 15.5102 7.87682 14.5764 6.94307C14.4864 6.84932 14.3627 6.79869 14.2314 6.79682ZM9.99957 21.1199C10.2696 21.1199 10.4796 21.3299 10.4796 21.5999C10.4796 21.8699 10.2696 22.0799 9.99957 22.0799C9.72957 22.0799 9.51957 21.8699 9.51957 21.5999C9.51957 21.3299 9.72957 21.1199 9.99957 21.1199Z" fill="#030504" fill-opacity="0.4"/>
|
||||
</svg>
|
After Width: | Height: | Size: 3.6 KiB |
3
components/svg/bootombar/unknown10.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 20" fill="none">
|
||||
<path d="M6.19515 0.864961C6.17452 0.868712 6.1539 0.874336 6.13515 0.879961C5.91202 0.930586 5.75452 1.13121 5.76015 1.35996V18.64C5.75827 18.8125 5.84827 18.9737 5.99827 19.0618C6.14827 19.1481 6.33202 19.1481 6.48202 19.0618C6.63202 18.9737 6.72202 18.8125 6.72015 18.64V1.35996C6.72577 1.22121 6.66952 1.08809 6.57015 0.994336C6.4689 0.898711 6.33202 0.851836 6.19515 0.864961ZM17.7151 0.864961C17.6945 0.868712 17.6739 0.874336 17.6551 0.879961C17.432 0.930586 17.2745 1.13121 17.2801 1.35996V18.64C17.2783 18.8125 17.3683 18.9737 17.5183 19.0618C17.6683 19.1481 17.852 19.1481 18.002 19.0618C18.152 18.9737 18.242 18.8125 18.2401 18.64V1.35996C18.2458 1.22121 18.1895 1.08809 18.0901 0.994336C17.9889 0.898711 17.852 0.851836 17.7151 0.864961ZM3.31515 5.18496C3.29452 5.18871 3.2739 5.19434 3.25515 5.19996C3.03202 5.25059 2.87452 5.45121 2.88015 5.67996V14.32C2.87827 14.4925 2.96827 14.6537 3.11827 14.7418C3.26827 14.8281 3.45202 14.8281 3.60202 14.7418C3.75202 14.6537 3.84202 14.4925 3.84015 14.32V5.67996C3.84577 5.54121 3.78952 5.40809 3.69015 5.31434C3.5889 5.21871 3.45202 5.17184 3.31515 5.18496ZM20.5951 5.18496C20.5745 5.18871 20.5539 5.19434 20.5351 5.19996C20.312 5.25059 20.1545 5.45121 20.1601 5.67996V14.32C20.1583 14.4925 20.2483 14.6537 20.3983 14.7418C20.5483 14.8281 20.732 14.8281 20.882 14.7418C21.032 14.6537 21.122 14.4925 21.1201 14.32V5.67996C21.1258 5.54121 21.0695 5.40809 20.9701 5.31434C20.8689 5.21871 20.732 5.17184 20.5951 5.18496ZM9.07515 5.66496C9.05452 5.66871 9.0339 5.67434 9.01515 5.67996C8.79202 5.73059 8.63452 5.93121 8.64015 6.15996V13.84C8.63827 14.0125 8.72827 14.1737 8.87827 14.2618C9.02827 14.3481 9.21202 14.3481 9.36202 14.2618C9.51202 14.1737 9.60202 14.0125 9.60015 13.84V6.15996C9.60577 6.02121 9.54952 5.88809 9.45015 5.79434C9.3489 5.69871 9.21202 5.65184 9.07515 5.66496ZM14.8351 5.66496C14.8145 5.66871 14.7939 5.67434 14.7751 5.67996C14.552 5.73059 14.3945 5.93121 14.4001 6.15996V13.84C14.3983 14.0125 14.4883 14.1737 14.6383 14.2618C14.7883 14.3481 14.972 14.3481 15.122 14.2618C15.272 14.1737 15.362 14.0125 15.3601 13.84V6.15996C15.3658 6.02121 15.3095 5.88809 15.2101 5.79434C15.1089 5.69871 14.972 5.65184 14.8351 5.66496ZM0.435146 8.06496C0.414521 8.06871 0.393896 8.07434 0.375146 8.07996C0.152021 8.13059 -0.00547904 8.33121 0.000145979 8.55996V11.44C-0.00172902 11.6125 0.0882711 11.7737 0.238271 11.8618C0.388271 11.9481 0.572021 11.9481 0.722021 11.8618C0.872021 11.7737 0.962021 11.6125 0.960146 11.44V8.55996C0.965771 8.42121 0.909521 8.28809 0.810146 8.19434C0.708896 8.09871 0.572021 8.05184 0.435146 8.06496ZM11.9551 8.06496C11.9345 8.06871 11.9139 8.07434 11.8951 8.07996C11.672 8.13059 11.5145 8.33121 11.5201 8.55996V11.44C11.5183 11.6125 11.6083 11.7737 11.7583 11.8618C11.9083 11.9481 12.092 11.9481 12.242 11.8618C12.392 11.7737 12.482 11.6125 12.4801 11.44V8.55996C12.4858 8.42121 12.4295 8.28809 12.3301 8.19434C12.2289 8.09871 12.092 8.05184 11.9551 8.06496ZM23.4751 8.06496C23.4545 8.06871 23.4339 8.07434 23.4151 8.07996C23.192 8.13059 23.0345 8.33121 23.0401 8.55996V11.44C23.0383 11.6125 23.1283 11.7737 23.2783 11.8618C23.4283 11.9481 23.612 11.9481 23.762 11.8618C23.912 11.7737 24.002 11.6125 24.0001 11.44V8.55996C24.0058 8.42121 23.9495 8.28809 23.8501 8.19434C23.7489 8.09871 23.612 8.05184 23.4751 8.06496Z" fill="#030504" fill-opacity="0.4"/>
|
||||
</svg>
|
After Width: | Height: | Size: 3.4 KiB |
3
components/svg/bootombar/unknown11.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M10.6584 0.959961C10.5447 0.960022 10.4346 1.00049 10.3479 1.07415C10.2612 1.14781 10.2035 1.24988 10.185 1.36215L9.71813 4.22059C9.15672 4.3839 8.6214 4.60425 8.11688 4.87871L5.75813 3.19402C5.66578 3.12808 5.55305 3.09698 5.43995 3.10625C5.32685 3.11552 5.2207 3.16455 5.14031 3.24465L3.27281 5.11027C3.19301 5.19013 3.1439 5.29557 3.13413 5.40804C3.12436 5.52052 3.15454 5.63284 3.21938 5.72527L4.87875 8.09902C4.60006 8.60649 4.37482 9.14484 4.2075 9.71152L1.36125 10.185C1.24932 10.2038 1.1477 10.2617 1.07441 10.3484C1.00113 10.4351 0.960925 10.5449 0.960938 10.6584V13.2984C0.960734 13.4112 1.00027 13.5205 1.07262 13.6071C1.14497 13.6937 1.2455 13.752 1.35656 13.7718L4.20469 14.2762C4.3711 14.8419 4.59491 15.3806 4.87406 15.8887L3.19406 18.239C3.12812 18.3314 3.09702 18.4441 3.10629 18.5572C3.11556 18.6703 3.16459 18.7765 3.24469 18.8568L5.11125 20.7243C5.19094 20.804 5.29613 20.8532 5.4084 20.8631C5.52067 20.8731 5.63286 20.8432 5.72531 20.7787L8.10375 19.1137C8.60976 19.3898 9.1464 19.6118 9.70969 19.7765L10.185 22.6387C10.2037 22.7508 10.2615 22.8527 10.3482 22.9261C10.4349 22.9996 10.5448 23.0399 10.6584 23.04H13.2984C13.4114 23.0401 13.5208 23.0005 13.6074 22.9279C13.694 22.8554 13.7522 22.7546 13.7719 22.6434L14.2809 19.77C14.8419 19.6027 15.376 19.3774 15.8794 19.0996L18.2738 20.7796C18.3662 20.8445 18.4785 20.8747 18.591 20.8649C18.7035 20.8551 18.8089 20.806 18.8888 20.7262L20.7553 18.8578C20.8358 18.7772 20.8849 18.6706 20.894 18.5571C20.9031 18.4435 20.8716 18.3305 20.805 18.2381L19.0978 15.8681C19.3708 15.3671 19.5907 14.8362 19.7541 14.279L22.6425 13.7728C22.7537 13.7531 22.8545 13.6949 22.927 13.6083C22.9996 13.5217 23.0392 13.4123 23.0391 13.2993V10.6593C23.039 10.5456 22.9985 10.4355 22.9249 10.3488C22.8512 10.2621 22.7491 10.2044 22.6369 10.1859L19.7531 9.71434C19.5886 9.1578 19.3681 8.62783 19.095 8.12715L20.7787 5.72809C20.8436 5.63566 20.8738 5.52333 20.864 5.41086C20.8542 5.29838 20.8051 5.19295 20.7253 5.11309L18.8578 3.24652C18.7772 3.16608 18.6706 3.11692 18.5571 3.10782C18.4436 3.09872 18.3305 3.13027 18.2381 3.19684L15.8747 4.89652C15.3711 4.61987 14.8367 4.3958 14.2744 4.22996L13.7719 1.35746C13.7524 1.24605 13.6943 1.14508 13.6077 1.07236C13.521 0.99964 13.4115 0.959834 13.2984 0.959961H10.6584ZM11.0672 1.91996H12.8953L13.3734 4.64902C13.3893 4.74055 13.4315 4.8255 13.4947 4.89356C13.5579 4.96163 13.6396 5.00988 13.7297 5.03246C14.4317 5.2074 15.091 5.4847 15.6928 5.84902C15.7733 5.89777 15.8663 5.92181 15.9603 5.91814C16.0543 5.91448 16.1451 5.88327 16.2216 5.8284L18.4678 4.21402L19.7606 5.50684L18.1612 7.7859C18.1078 7.86193 18.0776 7.95182 18.0743 8.04468C18.071 8.13754 18.0947 8.22937 18.1425 8.30902C18.5032 8.90947 18.7768 9.56462 18.9516 10.2628C18.9743 10.3537 19.0232 10.436 19.0922 10.4995C19.1611 10.5629 19.2472 10.6048 19.3397 10.62L22.0791 11.0681V12.8962L19.3341 13.3781C19.2425 13.3943 19.1575 13.4368 19.0896 13.5004C19.0217 13.5639 18.9738 13.6459 18.9516 13.7362C18.7788 14.4335 18.5051 15.0886 18.1444 15.689C18.096 15.7695 18.0722 15.8623 18.076 15.9562C18.0799 16.05 18.1111 16.1406 18.1659 16.2168L19.7878 18.4678L18.495 19.7615L16.2197 18.165C16.1433 18.1114 16.0529 18.0813 15.9597 18.0783C15.8665 18.0754 15.7744 18.0996 15.6947 18.1481C15.0959 18.5124 14.4359 18.791 13.7363 18.9684C13.6466 18.991 13.5653 19.039 13.5023 19.1067C13.4392 19.1744 13.3971 19.2589 13.3809 19.35L12.8963 22.08H11.0653L10.6134 19.3621C10.5981 19.2697 10.556 19.1838 10.4923 19.115C10.4287 19.0462 10.3463 18.9975 10.2553 18.975C9.55289 18.8018 8.89061 18.5249 8.28469 18.1612C8.20514 18.1136 8.11349 18.09 8.02082 18.0933C7.92815 18.0966 7.83843 18.1267 7.7625 18.18L5.505 19.7615L4.21125 18.4659L5.80688 16.2356C5.86141 16.1592 5.89234 16.0684 5.89584 15.9746C5.89933 15.8808 5.87525 15.788 5.82656 15.7078C5.45887 15.1002 5.18079 14.438 5.00531 13.7325C4.98286 13.6426 4.93489 13.5611 4.86719 13.4979C4.79949 13.4347 4.71494 13.3925 4.62375 13.3762L1.92094 12.8962V11.0653L4.62188 10.6162C4.71421 10.6009 4.80005 10.5589 4.86884 10.4955C4.93763 10.432 4.98637 10.3498 5.00906 10.259C5.18605 9.55107 5.464 8.88895 5.83031 8.28277C5.87862 8.20319 5.90274 8.11128 5.89975 8.01823C5.89675 7.92519 5.86676 7.83502 5.81344 7.75871L4.23656 5.50496L5.53125 4.21121L7.76906 5.81059C7.84519 5.86487 7.93552 5.89574 8.02895 5.89941C8.12238 5.90307 8.21485 5.87937 8.295 5.83121C8.89772 5.46922 9.56154 5.19456 10.2647 5.02215C10.3561 4.99965 10.4389 4.95077 10.5027 4.88159C10.5665 4.81242 10.6086 4.726 10.6238 4.63309L11.0672 1.91996ZM12 8.15996C9.88491 8.15996 8.16 9.88487 8.16 12C8.16 14.115 9.88491 15.84 12 15.84C14.1151 15.84 15.84 14.115 15.84 12C15.84 9.88487 14.1151 8.15996 12 8.15996ZM12 9.11996C13.5963 9.11996 14.88 10.4037 14.88 12C14.88 13.5962 13.5963 14.88 12 14.88C10.4037 14.88 9.12 13.5962 9.12 12C9.12 10.4037 10.4037 9.11996 12 9.11996Z" fill="#030504" fill-opacity="0.4"/>
|
||||
</svg>
|
After Width: | Height: | Size: 4.9 KiB |
3
components/svg/bootombar/unknown2.svg
Normal file
After Width: | Height: | Size: 5.0 KiB |
3
components/svg/bootombar/unknown3.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
|
||||
<path d="M8.96183 0.5875C8.94464 0.590625 8.92745 0.595313 8.91183 0.6C8.72589 0.642188 8.59464 0.809376 8.59933 1V3.4C8.59776 3.54375 8.67276 3.67813 8.79776 3.75156C8.92276 3.82344 9.07589 3.82344 9.20089 3.75156C9.32589 3.67813 9.40089 3.54375 9.39933 3.4V1C9.40401 0.884376 9.35714 0.773438 9.27433 0.695313C9.18995 0.615625 9.07589 0.576563 8.96183 0.5875ZM3.26183 2.9375C3.11183 2.96406 2.99151 3.07344 2.94933 3.21875C2.90714 3.36563 2.95089 3.52188 3.06183 3.625L4.76183 5.325C4.8587 5.44375 5.01339 5.49844 5.16339 5.46406C5.31183 5.42969 5.42901 5.3125 5.46339 5.16406C5.49776 5.01406 5.44308 4.85938 5.32433 4.7625L3.62433 3.0625C3.54151 2.97344 3.4212 2.92813 3.29933 2.9375C3.28683 2.9375 3.27433 2.9375 3.26183 2.9375ZM14.6118 2.9375C14.5212 2.95 14.4368 2.99531 14.3743 3.0625L12.6743 4.7625C12.5556 4.85938 12.5009 5.01406 12.5353 5.16406C12.5696 5.3125 12.6868 5.42969 12.8353 5.46406C12.9853 5.49844 13.14 5.44375 13.2368 5.325L14.9368 3.625C15.0618 3.50625 15.0978 3.32031 15.0243 3.16406C14.9493 3.00625 14.7837 2.91563 14.6118 2.9375ZM8.96183 5C8.94933 5.00313 8.93683 5.00781 8.92433 5.0125C8.89933 5.01406 8.87433 5.01875 8.84933 5.025C8.84464 5.02969 8.84151 5.03281 8.83683 5.0375C6.71495 5.12813 4.99933 6.85625 4.99933 9C4.99933 11.2016 6.79776 13 8.99933 13C11.2009 13 12.9993 11.2016 12.9993 9C12.9993 6.86406 11.2978 5.14219 9.18683 5.0375C9.17276 5.0375 9.16339 5.025 9.14933 5.025C9.1087 5.00938 9.06651 5.00156 9.02433 5C9.01651 5 9.00714 5 8.99933 5C8.98683 5 8.97433 5 8.96183 5ZM8.97433 5.8C8.98214 5.8 8.99151 5.8 8.99933 5.8C9.01183 5.8 9.02433 5.8 9.03683 5.8C10.7868 5.82031 12.1993 7.24531 12.1993 9C12.1993 10.7688 10.7681 12.2 8.99933 12.2C7.23214 12.2 5.79933 10.7688 5.79933 9C5.79933 7.24063 7.21808 5.81406 8.97433 5.8ZM0.886826 8.6C0.666514 8.63125 0.511826 8.83594 0.543076 9.05625C0.574326 9.27656 0.779014 9.43125 0.999326 9.4H3.39933C3.54308 9.40156 3.67745 9.32656 3.75089 9.20156C3.82276 9.07656 3.82276 8.92344 3.75089 8.79844C3.67745 8.67344 3.54308 8.59844 3.39933 8.6H0.999326C0.986826 8.6 0.974326 8.6 0.961826 8.6C0.949326 8.6 0.936826 8.6 0.924326 8.6C0.911826 8.6 0.899326 8.6 0.886826 8.6ZM14.4868 8.6C14.2665 8.63125 14.1118 8.83594 14.1431 9.05625C14.1743 9.27656 14.379 9.43125 14.5993 9.4H16.9993C17.1431 9.40156 17.2775 9.32656 17.3509 9.20156C17.4228 9.07656 17.4228 8.92344 17.3509 8.79844C17.2775 8.67344 17.1431 8.59844 16.9993 8.6H14.5993C14.5868 8.6 14.5743 8.6 14.5618 8.6C14.5493 8.6 14.5368 8.6 14.5243 8.6C14.5118 8.6 14.4993 8.6 14.4868 8.6ZM4.99933 12.55C4.9087 12.5625 4.82433 12.6078 4.76183 12.675L3.06183 14.375C2.94308 14.4719 2.88839 14.6266 2.92276 14.7766C2.95714 14.925 3.07433 15.0422 3.22276 15.0766C3.37276 15.1109 3.52745 15.0563 3.62433 14.9375L5.32433 13.2375C5.44308 13.1234 5.47901 12.9469 5.41495 12.7953C5.35245 12.6438 5.20089 12.5469 5.03683 12.55C5.02433 12.55 5.01183 12.55 4.99933 12.55ZM12.8743 12.55C12.7243 12.5766 12.604 12.6859 12.5618 12.8313C12.5196 12.9781 12.5634 13.1344 12.6743 13.2375L14.3743 14.9375C14.4712 15.0563 14.6259 15.1109 14.7759 15.0766C14.9243 15.0422 15.0415 14.925 15.0759 14.7766C15.1103 14.6266 15.0556 14.4719 14.9368 14.375L13.2368 12.675C13.1618 12.5953 13.0587 12.5516 12.9493 12.55C12.9368 12.55 12.9243 12.55 12.9118 12.55C12.8993 12.55 12.8868 12.55 12.8743 12.55ZM8.96183 14.1875C8.94464 14.1906 8.92745 14.1953 8.91183 14.2C8.72589 14.2422 8.59464 14.4094 8.59933 14.6V17C8.59776 17.1438 8.67276 17.2781 8.79776 17.3516C8.92276 17.4234 9.07589 17.4234 9.20089 17.3516C9.32589 17.2781 9.40089 17.1438 9.39933 17V14.6C9.40401 14.4844 9.35714 14.3734 9.27433 14.2953C9.18995 14.2156 9.07589 14.1766 8.96183 14.1875Z" fill="#030504" fill-opacity="0.8"/>
|
||||
</svg>
|
After Width: | Height: | Size: 3.7 KiB |
3
components/svg/bootombar/unknown4.svg
Normal file
After Width: | Height: | Size: 7.7 KiB |
3
components/svg/bootombar/unknown5.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M2.69965 0.800019C2.65903 0.806269 2.61215 0.826581 2.54965 0.862519C2.48715 0.898457 2.41528 0.970331 2.37465 1.06252C2.33403 1.15471 2.32622 1.25002 2.33715 1.31252C2.38247 1.56252 2.46215 1.55783 2.52465 1.62502C2.64809 1.75939 2.82465 1.92658 3.06215 2.17502C3.53559 2.67189 4.23247 3.47814 4.96215 4.77502C6.04341 6.69846 7.19184 9.68752 7.71215 14.2375C7.08559 12.7875 6.25434 11.5813 5.39965 10.6125C4.52465 9.62189 3.62778 8.88596 2.88715 8.38752C2.51684 8.13752 2.18715 7.94377 1.91215 7.81252C1.63715 7.68127 1.45903 7.60002 1.19965 7.60002C1.14653 7.60002 1.09028 7.60158 0.999654 7.63752C0.90903 7.67346 0.762154 7.78283 0.724654 7.92502C0.687154 8.06721 0.735592 8.16408 0.762154 8.22502C0.788717 8.28596 0.810592 8.32502 0.837154 8.36252C0.944967 8.50939 1.07622 8.64377 1.26215 8.83752C1.63403 9.22346 2.1934 9.81252 2.77465 10.675C3.93715 12.4016 5.19965 15.2 5.19965 19.575C5.19809 19.7188 5.27309 19.8531 5.39809 19.9266C5.52309 19.9985 5.67622 19.9985 5.80122 19.9266C5.92622 19.8531 6.00122 19.7188 5.99965 19.575C5.99965 15.0453 4.67465 12.0625 3.43715 10.225C3.08872 9.70783 2.83715 9.47033 2.53715 9.12502C3.2059 9.58596 4.01059 10.2563 4.79965 11.15C6.45122 13.0203 7.99966 15.8094 7.99966 19.575C8.00122 19.6 8.00591 19.625 8.01216 19.65C8.0184 19.6891 8.03091 19.7266 8.04965 19.7625C8.06528 19.7985 8.08715 19.8328 8.11215 19.8625C8.13091 19.8813 8.15278 19.8985 8.17465 19.9125C8.18716 19.9219 8.19966 19.9297 8.21215 19.9375C8.24809 19.9563 8.28559 19.9688 8.32466 19.975C8.37778 19.986 8.43403 19.986 8.48715 19.975C8.52778 19.9641 8.56528 19.9469 8.59966 19.925C8.60747 19.9219 8.61684 19.9172 8.62466 19.9125C8.64809 19.8938 8.66841 19.8735 8.68716 19.85C8.71684 19.825 8.74184 19.7953 8.76216 19.7625C8.7809 19.7235 8.79341 19.6813 8.79965 19.6375C8.80122 19.6172 8.80122 19.5953 8.79965 19.575C8.79965 11.7766 7.19497 7.11252 5.66215 4.38752C5.29965 3.74221 4.9434 3.21721 4.61215 2.77502C5.32934 3.37346 6.13716 4.17033 6.96215 5.33752C8.70122 7.79689 10.3731 11.636 10.7247 17.35C10.7231 17.3672 10.7231 17.3828 10.7247 17.4C10.7247 17.4047 10.7247 17.4078 10.7247 17.4125C10.7278 17.4203 10.7325 17.4297 10.7372 17.4375C10.7778 18.125 10.7997 18.8328 10.7997 19.575C10.7981 19.7188 10.8731 19.8531 10.9981 19.9266C11.1231 19.9985 11.2762 19.9985 11.4012 19.9266C11.5262 19.8531 11.6012 19.7188 11.5997 19.575C11.5997 18.811 11.5668 18.0844 11.5247 17.375C11.5278 17.35 11.5278 17.325 11.5247 17.3C11.5247 17.2953 11.5247 17.2922 11.5247 17.2875C11.6903 11.7266 13.234 8.61252 14.6247 6.87502C15.3215 6.00627 15.9825 5.47502 16.4372 5.16252C16.6637 5.00627 16.8293 4.91408 16.9497 4.83752C17.0106 4.79846 17.0418 4.83127 17.1622 4.66252C17.1918 4.62033 17.2403 4.53439 17.2372 4.40002C17.234 4.26564 17.1387 4.12658 17.0622 4.07502C16.909 3.97189 16.8559 4.00158 16.8122 4.00002C16.6012 3.99064 16.2512 4.02971 15.7122 4.18752C15.1731 4.34533 14.484 4.63127 13.7622 5.13752C12.5012 6.02033 11.1528 7.58439 10.2497 10.25C9.52466 8.01877 8.5809 6.24533 7.61215 4.87502C6.58559 3.42346 5.5309 2.42346 4.67465 1.77502C4.24653 1.45002 3.8684 1.22033 3.56215 1.06252C3.2559 0.904707 3.08247 0.800019 2.79965 0.800019C2.77153 0.800019 2.74028 0.793769 2.69965 0.800019ZM14.8247 5.50002C14.5575 5.75939 14.284 6.02033 13.9997 6.37502C12.8512 7.80783 11.6653 10.075 11.0872 13.6C10.9637 12.9188 10.8106 12.2797 10.6497 11.6625C10.6512 11.6578 10.6481 11.6547 10.6497 11.65C11.4793 8.37971 12.9403 6.68752 14.2247 5.78752C14.4497 5.62971 14.6137 5.61252 14.8247 5.50002ZM19.5997 7.60002C19.3403 7.60002 19.1637 7.69377 18.8997 7.82502C18.6356 7.95627 18.3262 8.13752 17.9747 8.38752C17.2715 8.88752 16.4106 9.63439 15.5872 10.625C13.9387 12.6078 12.3997 15.5953 12.3997 19.575C12.3981 19.7188 12.4731 19.8531 12.5981 19.9266C12.7231 19.9985 12.8762 19.9985 13.0012 19.9266C13.1262 19.8531 13.2012 19.7188 13.1997 19.575C13.1997 15.8016 14.659 13.0063 16.2122 11.1375C16.9887 10.2031 17.7903 9.50939 18.4372 9.05002C18.4465 9.04377 18.4528 9.04377 18.4622 9.03752C18.1715 9.39846 17.9137 9.66877 17.5622 10.2375C16.4293 12.0735 15.1997 15.0516 15.1997 19.575C15.1981 19.7188 15.2731 19.8531 15.3981 19.9266C15.5231 19.9985 15.6762 19.9985 15.8012 19.9266C15.9262 19.8531 16.0012 19.7188 15.9997 19.575C15.9997 15.1953 17.1715 12.3891 18.2372 10.6625C18.77 9.79846 19.2762 9.21252 19.6122 8.82502C19.7809 8.63127 19.9028 8.49689 19.9997 8.33752C20.0481 8.25783 20.1637 8.14377 20.0622 7.88752C20.0106 7.75939 19.8856 7.66877 19.7997 7.63752C19.7137 7.60627 19.6528 7.60002 19.5997 7.60002Z" fill="#030504" fill-opacity="0.8"/>
|
||||
</svg>
|
After Width: | Height: | Size: 4.6 KiB |
3
components/svg/bootombar/unknown6.svg
Normal file
After Width: | Height: | Size: 7.4 KiB |
3
components/svg/bootombar/unknown7.svg
Normal file
After Width: | Height: | Size: 5.9 KiB |
3
components/svg/bootombar/unknown8.svg
Normal file
After Width: | Height: | Size: 7.5 KiB |
3
components/svg/bootombar/unknown9.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="16" viewBox="0 0 20 16" fill="none">
|
||||
<path d="M5.16262 0.387549C5.14543 0.390674 5.12825 0.395362 5.11262 0.400049C4.92668 0.442237 4.79543 0.609424 4.80012 0.800049V15.2C4.79856 15.3438 4.87356 15.4782 4.99856 15.5516C5.12356 15.6235 5.27668 15.6235 5.40168 15.5516C5.52668 15.4782 5.60168 15.3438 5.60012 15.2V0.800049C5.60481 0.684424 5.55793 0.573487 5.47512 0.495361C5.39075 0.415674 5.27668 0.376612 5.16262 0.387549ZM14.7626 0.387549C14.7454 0.390674 14.7282 0.395362 14.7126 0.400049C14.5267 0.442237 14.3954 0.609424 14.4001 0.800049V15.2C14.3986 15.3438 14.4736 15.4782 14.5986 15.5516C14.7236 15.6235 14.8767 15.6235 15.0017 15.5516C15.1267 15.4782 15.2017 15.3438 15.2001 15.2V0.800049C15.2048 0.684424 15.1579 0.573487 15.0751 0.495361C14.9907 0.415674 14.8767 0.376612 14.7626 0.387549ZM2.76262 3.98755C2.74543 3.99067 2.72825 3.99536 2.71262 4.00005C2.52668 4.04224 2.39543 4.20942 2.40012 4.40005V11.6C2.39856 11.7438 2.47356 11.8782 2.59856 11.9516C2.72356 12.0235 2.87668 12.0235 3.00168 11.9516C3.12668 11.8782 3.20168 11.7438 3.20012 11.6V4.40005C3.20481 4.28442 3.15793 4.17349 3.07512 4.09536C2.99075 4.01567 2.87668 3.97661 2.76262 3.98755ZM17.1626 3.98755C17.1454 3.99067 17.1282 3.99536 17.1126 4.00005C16.9267 4.04224 16.7954 4.20942 16.8001 4.40005V11.6C16.7986 11.7438 16.8736 11.8782 16.9986 11.9516C17.1236 12.0235 17.2767 12.0235 17.4017 11.9516C17.5267 11.8782 17.6017 11.7438 17.6001 11.6V4.40005C17.6048 4.28442 17.5579 4.17349 17.4751 4.09536C17.3907 4.01567 17.2767 3.97661 17.1626 3.98755ZM7.56262 4.38755C7.54543 4.39067 7.52825 4.39536 7.51262 4.40005C7.32668 4.44224 7.19543 4.60942 7.20012 4.80005V8.00005V11.2C7.19856 11.3438 7.27356 11.4782 7.39856 11.5516C7.52356 11.6235 7.67668 11.6235 7.80168 11.5516C7.92668 11.4782 8.00168 11.3438 8.00012 11.2V4.80005C8.00481 4.68442 7.95793 4.57349 7.87512 4.49536C7.79075 4.41567 7.67668 4.37661 7.56262 4.38755ZM12.3626 4.38755C12.3454 4.39067 12.3282 4.39536 12.3126 4.40005C12.1267 4.44224 11.9954 4.60942 12.0001 4.80005V11.2C11.9986 11.3438 12.0736 11.4782 12.1986 11.5516C12.3236 11.6235 12.4767 11.6235 12.6017 11.5516C12.7267 11.4782 12.8017 11.3438 12.8001 11.2V4.80005C12.8048 4.68442 12.7579 4.57349 12.6751 4.49536C12.5907 4.41567 12.4767 4.37661 12.3626 4.38755ZM0.362622 6.38755C0.345434 6.39067 0.328247 6.39536 0.312622 6.40005C0.126684 6.44224 -0.00456587 6.60942 0.000121649 6.80005V9.20005C-0.00144085 9.3438 0.0735592 9.47817 0.198559 9.55161C0.323559 9.62349 0.476684 9.62349 0.601684 9.55161C0.726684 9.47817 0.801684 9.3438 0.800122 9.20005V6.80005C0.804809 6.68442 0.757934 6.57349 0.675122 6.49536C0.590747 6.41567 0.476684 6.37661 0.362622 6.38755ZM9.96262 6.38755C9.94543 6.39067 9.92825 6.39536 9.91262 6.40005C9.72668 6.44224 9.59543 6.60942 9.60012 6.80005V9.20005C9.59856 9.3438 9.67356 9.47817 9.79856 9.55161C9.92356 9.62349 10.0767 9.62349 10.2017 9.55161C10.3267 9.47817 10.4017 9.3438 10.4001 9.20005V6.80005C10.4048 6.68442 10.3579 6.57349 10.2751 6.49536C10.1907 6.41567 10.0767 6.37661 9.96262 6.38755ZM19.5626 6.38755C19.5454 6.39067 19.5282 6.39536 19.5126 6.40005C19.3267 6.44224 19.1954 6.60942 19.2001 6.80005V9.20005C19.1986 9.3438 19.2736 9.47817 19.3986 9.55161C19.5236 9.62349 19.6767 9.62349 19.8017 9.55161C19.9267 9.47817 20.0017 9.3438 20.0001 9.20005V6.80005C20.0048 6.68442 19.9579 6.57349 19.8751 6.49536C19.7907 6.41567 19.6767 6.37661 19.5626 6.38755Z" fill="#030504" fill-opacity="0.8"/>
|
||||
</svg>
|
After Width: | Height: | Size: 3.4 KiB |
9
components/svg/homebar/ForestSVG.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10.742 9.768v.211a3.158 3.158 0 0 1-1.157 6.105s-3.375.018-4.106 0A3.158 3.158 0 0 1 4.427 9.98v-.21a3.158 3.158 0 1 1 6.316 0Zm-3.157 6.316V22.4Zm6.315 3.158V22.4Z" /><path d="M7.585 16.084V22.4m6.315-3.158V22.4M10.742 9.768v.211a3.158 3.158 0 0 1-1.157 6.105s-3.375.018-4.106 0A3.158 3.158 0 0 1 4.427 9.98v-.21a3.158 3.158 0 1 1 6.316 0Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /><path d="M12.848 19.242h8.737a1.053 1.053 0 0 0 .736-1.79l-3.157-3.473h.315a1.053 1.053 0 0 0 .737-1.79l-3.158-3.473h.21a1.052 1.052 0 0 0 .843-1.79L13.901 2.4l-1.474 1.579" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ForestSVG'
|
||||
}
|
||||
</script>
|
9
components/svg/homebar/MeadowSVG.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M5.729 2.261a1.111 1.111 0 0 1 1.298-.096c3.84 2.363 5.287 4.602 6.41 7.893.277.814.895 3.102 1.063 3.942 1.143-1.922 3.226-4.153 5.977-5.253a1.111 1.111 0 0 1 1.476 1.35l-.2.67C20.24 15.8 19.777 17.34 19.777 20.89a1.111 1.111 0 1 1-2.222 0c0-3.395.796-5.65 1.896-9.363-1.246.826-2.9 2.41-3.452 3.474-.85 1.638-.667 3.45-.667 5.889a1.111 1.111 0 1 1-2.222 0c0-3.325-.144-6.497-1.12-9.363C11.334 9.605 9.661 6.612 8 5c.611 2.32 1.197 5.132 1.5 8 .257 2.434.278 4.856.278 7.606v.283c0 .295-.063.541-.278.786-.154.174-.29.27-.5.325-.34.088-.63-.117-.839-.325-.208-.209-.28-.491-.28-.786 0-2.544.249-4.935-.381-6.889-.362-1.126-2.063-2.662-3-3.5 1.022 3.503 1.62 5.736 1.62 9.5 0 .6 0 1.25-.12 1.5-.14.294-.372.5-.667.5a1.111 1.111 0 0 1-1.11-1.111c0-4.293-.525-6.95-2.166-11.871a1.111 1.111 0 0 1 1.466-1.382C5.073 8.255 6.611 10.023 7.5 11c-.296-2.242-1.236-5.38-2.087-7.475A1.111 1.111 0 0 1 5.73 2.26Z" fill="currentColor" /></svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'MeadowSVG'
|
||||
}
|
||||
</script>
|
9
components/svg/homebar/TropicsSVG.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M11.988 2.002a.55.55 0 0 0-.06.015.476.476 0 0 0-.37.476v2.853c-.002.171.087.331.235.418.149.086.33.086.479 0a.479.479 0 0 0 .235-.418V2.493a.473.473 0 0 0-.519-.49Zm-6.76 2.794a.472.472 0 0 0-.37.335c-.05.175.002.36.134.483l2.016 2.021a.476.476 0 1 0 .667-.668L5.659 4.945a.475.475 0 0 0-.386-.149H5.23Zm13.46 0a.476.476 0 0 0-.282.15l-2.016 2.02a.477.477 0 0 0 .19.834.476.476 0 0 0 .477-.165l2.016-2.021a.474.474 0 0 0-.385-.817Zm-6.7 2.453c-.015.004-.03.01-.044.015a.493.493 0 0 0-.09.015l-.014.015c-2.516.107-4.55 2.162-4.55 4.711 0 2.618 2.132 4.757 4.743 4.757 2.61 0 4.743-2.139 4.743-4.757 0-2.54-2.018-4.587-4.521-4.711-.017 0-.028-.015-.045-.015a.459.459 0 0 0-.148-.03h-.074Zm.015.951h.074a3.8 3.8 0 0 1 3.75 3.805 3.798 3.798 0 0 1-3.794 3.805 3.799 3.799 0 0 1-3.795-3.805A3.8 3.8 0 0 1 12.003 8.2Zm-9.59 3.33a.481.481 0 0 0 .133.95h2.846a.477.477 0 1 0 0-.95h-2.98Zm16.127 0a.481.481 0 0 0 .133.95h2.846a.477.477 0 1 0 0-.95H18.54ZM7.29 16.227a.476.476 0 0 0-.282.148l-2.016 2.022a.478.478 0 0 0 .19.834.476.476 0 0 0 .477-.166l2.016-2.021a.475.475 0 0 0-.341-.817h-.045Zm9.338 0a.472.472 0 0 0-.371.334c-.05.174.002.36.133.483l2.016 2.021a.476.476 0 0 0 .832-.19.478.478 0 0 0-.165-.478l-2.016-2.022a.474.474 0 0 0-.34-.148h-.089Zm-4.64 1.947a.54.54 0 0 0-.06.015.476.476 0 0 0-.37.475v2.854a.479.479 0 0 0 .235.418c.149.085.33.085.479 0a.479.479 0 0 0 .235-.418v-2.854a.473.473 0 0 0-.519-.49Z" fill="currentColor" stroke="currentColor" stroke-width="1.2" /></svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'TropicsSVG'
|
||||
}
|
||||
</script>
|
9
components/svg/homebar/WaveSVG.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M14.635 2c-2.084 0-3.74.848-5.065 2.091C8.245 5.334 7.235 6.96 6.347 8.556c-.888 1.596-1.657 3.17-2.447 4.306-.79 1.136-1.546 1.786-2.454 1.786a.4.4 0 0 0-.17.035.44.44 0 0 0-.145.105.532.532 0 0 0 0 .693c.041.045.09.08.145.105a.405.405 0 0 0 .17.035c1.29 0 2.292-.931 3.151-2.167.859-1.235 1.628-2.823 2.499-4.389.87-1.566 1.839-3.102 3.042-4.23 1.203-1.13 2.624-1.862 4.497-1.862.647 0 1.703.18 2.535.647.831.467 1.421 1.133 1.421 2.272 0 1.459-2.03 2.018-2.695.731a.469.469 0 0 0-.142-.166.401.401 0 0 0-.4-.044.442.442 0 0 0-.17.131c-.331.419-.604.545-.833.565-.23.02-.459-.074-.687-.266a2.508 2.508 0 0 1-.579-.762c-.142-.283-.209-.574-.209-.676a.53.53 0 0 0-.126-.348.44.44 0 0 0-.144-.107.403.403 0 0 0-.34 0 .44.44 0 0 0-.142.107.531.531 0 0 0-.127.348c0 .385.126.763.32 1.149.195.386.466.768.815 1.062.186.157.4.286.632.37.085 2.077 1.132 3.96 2.685 5.325a9.305 9.305 0 0 0 6.1 2.31.405.405 0 0 0 .17-.035.44.44 0 0 0 .144-.105.532.532 0 0 0 0-.693.442.442 0 0 0-.145-.105.4.4 0 0 0-.17-.035 8.506 8.506 0 0 1-5.55-2.099c-1.374-1.207-2.246-2.807-2.343-4.519.274-.083.544-.26.807-.493 1.295 1.498 4.009.587 4.009-1.645 0-1.537-.894-2.574-1.903-3.14C16.558 2.185 15.417 2 14.635 2ZM11.99 8.804a.42.42 0 0 0-.308.147.516.516 0 0 0-.125.346c0 1.135.486 2.293 1.122 3.316.635 1.022 1.426 1.905 2.152 2.44a.417.417 0 0 0 .332.077.415.415 0 0 0 .16-.073.463.463 0 0 0 .123-.135.52.52 0 0 0 .065-.369.515.515 0 0 0-.068-.176.46.46 0 0 0-.125-.134c-.593-.438-1.34-1.258-1.915-2.181-.573-.924-.966-1.954-.966-2.765a.53.53 0 0 0-.13-.351.438.438 0 0 0-.145-.107.403.403 0 0 0-.172-.035Zm-2.625 8.269c-.17 0-.326.11-.398.28 0 0-.764 1.674-2.24 1.674-1.475 0-2.239-1.673-2.239-1.673a.435.435 0 0 0-.44-.275l-.04.001a.453.453 0 0 0-.317.274s-.786 1.673-2.24 1.673a.435.435 0 0 0-.385.24.538.538 0 0 0 0 .492c.08.152.228.243.386.241 1.499 0 2.248-.993 2.638-1.628C4.478 19.01 5.22 20 6.727 20c1.508 0 2.25-.99 2.638-1.628.387.637 1.13 1.628 2.638 1.628s2.251-.99 2.638-1.628c.386.637 1.128 1.628 2.638 1.628s2.251-.99 2.637-1.628c.389.635 1.137 1.628 2.638 1.628a.436.436 0 0 0 .387-.241.537.537 0 0 0 0-.492.434.434 0 0 0-.387-.24c-1.456 0-2.24-1.673-2.24-1.673a.44.44 0 0 0-.398-.281c-.17 0-.326.11-.398.28 0 0-.762 1.674-2.24 1.674-1.476 0-2.239-1.673-2.239-1.673a.44.44 0 0 0-.398-.281c-.17 0-.326.11-.399.28 0 0-.762 1.674-2.239 1.674-1.477 0-2.24-1.673-2.24-1.673a.44.44 0 0 0-.398-.281Z" fill="currentColor" stroke="currentColor" /></svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'WaveSVG'
|
||||
}
|
||||
</script>
|
3
components/svg/homebar/foucusedIconHomebar.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="24" viewBox="0 0 19 20" fill="none">
|
||||
<path d="M2.69824 0.055674C1.5521 0.101019 0.5 1.0361 0.5 2.29591V17.7041C0.5 19.3839 2.37138 20.486 3.84082 19.6719L17.7451 11.9678C19.2568 11.1301 19.2568 8.86988 17.7451 8.03224L3.84082 0.328135C3.47346 0.124617 3.08029 0.0405589 2.69824 0.055674ZM2.71582 1.53321C2.84512 1.53266 2.98064 1.56618 3.11328 1.63966L17.0186 9.34376C17.5678 9.64811 17.5678 10.3519 17.0186 10.6563L3.11328 18.3604C2.58272 18.6543 2 18.3104 2 17.7041V2.29591C2 1.99278 2.14535 1.75582 2.35742 1.63087C2.46346 1.5684 2.58652 1.53377 2.71582 1.53321Z" fill="#030504" fill-opacity="0.4"/>
|
||||
</svg>
|
After Width: | Height: | Size: 672 B |
3
components/svg/homebar/unknown1home.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="24" viewBox="0 0 20 24" fill="none">
|
||||
<path d="M8.07207 0.952443C7.8077 0.958068 7.59582 1.17557 7.59957 1.43994V14.3999C7.59957 14.9999 7.6802 15.5681 7.98395 16.0349C8.2877 16.5018 8.8502 16.7999 9.51957 16.7999V20.2499C8.96457 20.4506 8.55957 20.9793 8.55957 21.5999C8.55957 22.3893 9.2102 23.0399 9.99957 23.0399C10.7889 23.0399 11.4396 22.3893 11.4396 21.5999C11.4396 20.9793 11.0346 20.4506 10.4796 20.2499V16.7999C11.0964 16.7999 11.6346 16.5018 11.9514 16.0499C12.2702 15.5962 12.3996 15.0206 12.3996 14.3999V1.43994C12.4014 1.26744 12.3114 1.10619 12.1614 1.01807C12.0114 0.931818 11.8277 0.931818 11.6777 1.01807C11.5277 1.10619 11.4377 1.26744 11.4396 1.43994V14.3999C11.4396 14.8818 11.3289 15.2662 11.1677 15.4968C11.0046 15.7293 10.8227 15.8399 10.4796 15.8399H9.51957C9.08645 15.8399 8.93082 15.7293 8.7902 15.5118C8.6477 15.2943 8.55957 14.9024 8.55957 14.3999V1.43994C8.56145 1.31057 8.51082 1.18494 8.41895 1.09307C8.32707 1.00119 8.20145 0.950568 8.07207 0.952443ZM3.0377 3.83619C2.91207 3.83807 2.79394 3.89057 2.7077 3.98057C0.27582 6.41244 0.27582 10.3724 2.7077 12.8043C2.8277 12.9299 3.0077 12.9806 3.17457 12.9374C3.34332 12.8943 3.47457 12.7612 3.5177 12.5943C3.5627 12.4256 3.51207 12.2474 3.38645 12.1274C1.3202 10.0593 1.3202 6.72557 3.38645 4.65932C3.52895 4.52057 3.5702 4.31057 3.49332 4.12869C3.41645 3.94494 3.23644 3.82869 3.0377 3.83619ZM16.9464 4.08182C16.7514 4.08182 16.5771 4.19994 16.5039 4.38182C16.4308 4.56182 16.4739 4.76994 16.6146 4.90494C18.6789 6.97119 18.6789 10.3068 16.6146 12.3731C16.4889 12.4931 16.4383 12.6731 16.4814 12.8399C16.5246 13.0087 16.6577 13.1399 16.8246 13.1831C16.9933 13.2281 17.1714 13.1774 17.2914 13.0518C19.7252 10.6181 19.7252 6.65994 17.2914 4.22619C17.2014 4.13432 17.0777 4.08182 16.9464 4.08182ZM4.3952 5.19182C4.26957 5.19557 4.15144 5.24807 4.0652 5.33807C2.38144 7.02182 2.38144 9.76494 4.0652 11.4468C4.1852 11.5724 4.3652 11.6231 4.53207 11.5799C4.70082 11.5368 4.83207 11.4037 4.8752 11.2368C4.9202 11.0681 4.86957 10.8899 4.74395 10.7699C3.4277 9.45369 3.4277 7.33307 4.74395 6.01682C4.88645 5.87807 4.9277 5.66807 4.85082 5.48619C4.77395 5.30244 4.59395 5.18619 4.3952 5.19182ZM15.5889 5.43932C15.3939 5.43932 15.2196 5.55744 15.1446 5.73932C15.0714 5.91932 15.1146 6.12744 15.2552 6.26244C16.5714 7.57869 16.5714 9.69932 15.2552 11.0156C15.1296 11.1356 15.0789 11.3156 15.1239 11.4824C15.1671 11.6512 15.2983 11.7824 15.4671 11.8256C15.6339 11.8706 15.8139 11.8199 15.9339 11.6943C17.6177 10.0106 17.6177 7.26744 15.9339 5.58557C15.8439 5.49182 15.7202 5.43932 15.5889 5.43932ZM5.7527 6.54932C5.62707 6.55307 5.50895 6.60557 5.4227 6.69557C4.48895 7.62932 4.48895 9.15744 5.4227 10.0893C5.5427 10.2149 5.7227 10.2656 5.88957 10.2224C6.05832 10.1793 6.18957 10.0462 6.2327 9.87932C6.2777 9.71057 6.22707 9.53244 6.10145 9.41244C5.5352 8.84432 5.5352 7.94057 6.10145 7.37432C6.24395 7.23557 6.2852 7.02557 6.20832 6.84369C6.13144 6.65994 5.95145 6.54369 5.7527 6.54932ZM14.2314 6.79682C14.0364 6.79869 13.8621 6.91682 13.7871 7.09682C13.7139 7.27869 13.7571 7.48494 13.8977 7.62182C14.4639 8.18807 14.4639 9.09182 13.8977 9.65807C13.7721 9.77807 13.7214 9.95807 13.7664 10.1249C13.8096 10.2937 13.9408 10.4249 14.1096 10.4681C14.2764 10.5131 14.4564 10.4624 14.5764 10.3368C15.5102 9.40307 15.5102 7.87682 14.5764 6.94307C14.4864 6.84932 14.3627 6.79869 14.2314 6.79682ZM9.99957 21.1199C10.2696 21.1199 10.4796 21.3299 10.4796 21.5999C10.4796 21.8699 10.2696 22.0799 9.99957 22.0799C9.72957 22.0799 9.51957 21.8699 9.51957 21.5999C9.51957 21.3299 9.72957 21.1199 9.99957 21.1199Z" fill="#030504" fill-opacity="0.4"/>
|
||||
</svg>
|
After Width: | Height: | Size: 3.6 KiB |
1
components/svg/login/man.svg
Normal file
After Width: | Height: | Size: 44 KiB |
9
components/svg/sounds/ForestTitleSvg.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<svg width="30" height="29" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M8.716 19.421V27m7.56-3.79V27m-3.78-15.158v.253a3.783 3.783 0 0 1 1.946 1.693 3.798 3.798 0 0 1-.903 4.772 3.774 3.774 0 0 1-2.429.861s-4.04.022-4.914 0a3.775 3.775 0 0 1-2.377-.92 3.792 3.792 0 0 1-.816-4.74 3.783 3.783 0 0 1 1.933-1.666v-.253c0-1.005.398-1.969 1.107-2.68a3.775 3.775 0 0 1 5.346 0 3.794 3.794 0 0 1 1.107 2.68Z" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" /><path d="M15.016 23.21h10.458a1.258 1.258 0 0 0 1.148-.782 1.267 1.267 0 0 0-.265-1.365l-3.78-4.168h.377a1.258 1.258 0 0 0 1.148-.782 1.266 1.266 0 0 0-.266-1.366l-3.78-4.168h.252a1.257 1.257 0 0 0 1.254-.72 1.267 1.267 0 0 0-.246-1.427L16.276 3l-1.764 1.895" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ForestTitleSVG'
|
||||
}
|
||||
</script>
|
9
components/svg/sounds/MeadowTitleSvg.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<svg width="29" height="29" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6.472 1.267c.233-.196.613-.277 1.027-.266.707.019 1.327.677 1.573.9 4.496 4.045 6.76 6.08 7.824 10.457.718 2.955.905 5.007 1.124 6.102 1.49-2.506 3.65-6.879 7.237-8.313.253-.101.632-.274.9-.225.267.049.473.041.674.225.2.184.311.187.384.45.073.262.078.413 0 .674l-.42 1.087c-1.97 6.56-3.155 8.803-2.93 13.52 0 .384.001.51-.181.75-.056.072-.334.374-.719.374-.384 0-.633-.032-.855-.375-.185-.287-.218-.69-.218-1.073 0-4.426 1.887-8.771 3.321-13.613-1.625 1.078-4.227 4.458-4.945 5.845-1.108 2.135-1.698 4.588-1.698 7.767 0 .385-.053.721-.281 1.074-.177.274-.36.375-.743.375-.384 0-.62-.26-.83-.6-.18-.289-.195-.464-.195-.849 0-4.334-.78-10.998-2.054-14.736-.854-2.505-3.988-5.316-6.155-7.418 1.473 5.592 3.439 13.977 3.439 21.785v.37c0 .384.052.733-.206 1.073-.19.253-.418.371-.799.375-.377.004-.576-.148-.774-.375-.296-.336-.225-.74-.225-1.124 0-3.317-.072-6.289-.893-8.837-.473-1.468-4.036-5.427-5.257-6.52C4.929 14.715 6.6 20.749 6.6 25.554c0 .385.056.75-.225 1.074-.197.228-.373.375-.674.375-.302 0-.476-.148-.675-.375-.295-.336-.225-.74-.225-1.124 0-5.597-.977-9.01-3.117-15.427a1.448 1.448 0 0 1 1.913-1.802c2.02.807 4.098 4.067 5.257 5.34-.386-2.923-1.685-7.967-2.793-10.7a1.449 1.449 0 0 1 .411-1.647Z" fill="currentColor" /></svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'MeadowTitleSVG'
|
||||
}
|
||||
</script>
|
9
components/svg/sounds/TropicsTitleSvg.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<svg width="29" height="29" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#a)"><path d="M14.52 1.003a.751.751 0 0 0-.08.02.643.643 0 0 0-.501.642v3.853a.646.646 0 0 0 .318.564c.201.115.447.115.647 0a.646.646 0 0 0 .319-.564V1.665a.642.642 0 0 0-.702-.662ZM5.37 4.775a.638.638 0 0 0-.5.452.645.645 0 0 0 .18.652l2.729 2.729c.155.19.403.278.644.223a.646.646 0 0 0 .482-.482.645.645 0 0 0-.223-.644l-2.73-2.73a.645.645 0 0 0-.521-.2h-.06Zm18.22 0a.645.645 0 0 0-.38.2l-2.73 2.73a.645.645 0 0 0-.223.644.645.645 0 0 0 .482.482c.24.055.489-.033.644-.223l2.729-2.73a.642.642 0 0 0-.522-1.104Zm-9.07 3.311c-.02.005-.04.013-.06.02a.67.67 0 0 0-.12.02l-.02.02c-3.406.146-6.16 2.92-6.16 6.361 0 3.534 2.887 6.421 6.42 6.421 3.535 0 6.422-2.887 6.422-6.42 0-3.43-2.732-6.194-6.12-6.362-.023 0-.038-.02-.06-.02a.621.621 0 0 0-.201-.04h-.1Zm.02 1.284h.101a5.138 5.138 0 1 1-.1 0ZM1.559 13.865a.65.65 0 0 0 .18 1.284h3.853a.646.646 0 0 0 .565-.318.648.648 0 0 0 0-.647.646.646 0 0 0-.565-.319H1.56Zm21.832 0a.65.65 0 0 0 .18 1.284h3.853a.646.646 0 0 0 .564-.318.648.648 0 0 0 0-.647.646.646 0 0 0-.564-.319H23.39Zm-15.23 6.34a.645.645 0 0 0-.381.201l-2.73 2.73a.645.645 0 0 0-.223.644.646.646 0 0 0 .482.482c.24.055.49-.033.645-.224l2.729-2.729a.64.64 0 0 0-.462-1.103h-.06Zm12.641 0a.638.638 0 0 0-.501.452.645.645 0 0 0 .18.652l2.73 2.73c.155.19.403.278.644.223a.646.646 0 0 0 .481-.482.645.645 0 0 0-.223-.645l-2.729-2.729a.642.642 0 0 0-.461-.2h-.12Zm-6.28 2.63a.73.73 0 0 0-.08.02.643.643 0 0 0-.502.642v3.852a.646.646 0 0 0 .318.565c.201.115.447.115.647 0a.646.646 0 0 0 .319-.565v-3.852a.642.642 0 0 0-.702-.662Z" fill="currentColor" stroke="currentColor" stroke-width="1.2" /></g><defs><clipPath id="a"><path d="M0 0h29v29H0z" /></clipPath></defs></svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'TropicsTitleSVG'
|
||||
}
|
||||
</script>
|
9
components/svg/sounds/WaveTitleSvg.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<svg width="35" height="29" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M20.973 3c-2.747 0-4.93 1.084-6.677 2.672-1.746 1.588-3.077 3.665-4.248 5.705-1.17 2.04-2.184 4.05-3.225 5.502-1.041 1.451-2.038 2.282-3.235 2.282a.543.543 0 0 0-.225.045.58.58 0 0 0-.19.134.661.661 0 0 0 0 .885.548.548 0 0 0 .415.179c1.7 0 3.022-1.189 4.154-2.768 1.132-1.578 2.146-3.608 3.293-5.608 1.148-2 2.425-3.964 4.01-5.407 1.587-1.442 3.46-2.378 5.928-2.378.853 0 2.245.23 3.341.827 1.097.597 1.875 1.448 1.875 2.903 0 1.864-2.677 2.579-3.553.935a.605.605 0 0 0-.188-.213.555.555 0 0 0-.528-.057.581.581 0 0 0-.224.169c-.436.534-.795.696-1.098.72-.302.026-.604-.094-.904-.34a3.216 3.216 0 0 1-.764-.973c-.187-.361-.275-.733-.275-.863a.662.662 0 0 0-.167-.445.58.58 0 0 0-.189-.137.546.546 0 0 0-.636.137.629.629 0 0 0-.167.445c0 .492.166.975.422 1.468s.614.98 1.074 1.357c.245.2.527.365.833.472.112 2.654 1.493 5.06 3.54 6.805a12.49 12.49 0 0 0 8.04 2.951.549.549 0 0 0 .415-.179.66.66 0 0 0 0-.885.58.58 0 0 0-.191-.134.544.544 0 0 0-.225-.045c-2.762 0-5.396-1.044-7.317-2.682-1.81-1.542-2.96-3.587-3.088-5.773.362-.107.718-.333 1.064-.63 1.708 1.913 5.285.749 5.285-2.103 0-1.964-1.178-3.289-2.508-4.013-1.33-.724-2.836-.96-3.867-.96Zm-3.486 8.694a.562.562 0 0 0-.407.188.647.647 0 0 0-.163.442c0 1.45.64 2.93 1.478 4.237.837 1.306 1.88 2.434 2.836 3.117a.558.558 0 0 0 .438.1.556.556 0 0 0 .212-.093.598.598 0 0 0 .162-.173.65.65 0 0 0 .085-.472.647.647 0 0 0-.09-.225.598.598 0 0 0-.164-.17c-.783-.56-1.768-1.609-2.524-2.788-.756-1.18-1.274-2.497-1.274-3.533a.66.66 0 0 0-.17-.449.576.576 0 0 0-.192-.136.546.546 0 0 0-.227-.045Zm-3.46 10.565a.581.581 0 0 0-.525.36s-1.007 2.138-2.952 2.138-2.952-2.138-2.952-2.138a.575.575 0 0 0-.58-.351 1.64 1.64 0 0 1-.054.001.592.592 0 0 0-.416.35s-1.037 2.138-2.952 2.138a.576.576 0 0 0-.51.307.668.668 0 0 0 0 .628c.107.194.302.31.51.308 1.976 0 2.963-1.268 3.477-2.08.511.814 1.49 2.08 3.477 2.08 1.988 0 2.965-1.266 3.477-2.08.51.814 1.49 2.08 3.477 2.08 1.988 0 2.968-1.266 3.477-2.08.51.814 1.487 2.08 3.477 2.08 1.99 0 2.968-1.266 3.477-2.08.512.811 1.499 2.08 3.477 2.08a.578.578 0 0 0 .51-.308.669.669 0 0 0 0-.628.575.575 0 0 0-.51-.307c-1.92 0-2.951-2.138-2.951-2.138a.58.58 0 0 0-.526-.36.581.581 0 0 0-.525.36s-1.005 2.138-2.952 2.138-2.952-2.138-2.952-2.138a.58.58 0 0 0-.525-.36.582.582 0 0 0-.525.36s-1.005 2.138-2.952 2.138-2.952-2.138-2.952-2.138a.58.58 0 0 0-.525-.36Z" fill="currentColor" stroke="currentColor" /></svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'WaveTitleSVG'
|
||||
}
|
||||
</script>
|
166
components/timer/PomodoroTimer.vue
Normal file
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<div class="timer">
|
||||
<div class="timer__phases">
|
||||
<p class="tag is-focus" :class="{'is-active' : timer.settings.timer[timer.getCurrentSession].text === 'Focus'}">Focus</p>
|
||||
<p class="tag is-short" :class="{'is-active' : timer.settings.timer[timer.getCurrentSession].text === 'Short Break'}">Short Break</p>
|
||||
<p class="tag is-long" :class="{'is-active' : timer.settings.timer[timer.getCurrentSession].text === 'Long Break'}">Long Break</p>
|
||||
</div>
|
||||
<div class="timer__clock" :data-round="(timer.getCurrentSessionNumber) " :model-value="(timer.getTimeRemaining * 100) / (timer.getSessionTime * 60)" :style="'color:' + timer.getCurrentColor">
|
||||
{{ String(getMinutes()).padStart(2, '0') }}:{{ String(getSeconds()).padStart(2, '0') }}
|
||||
</div>
|
||||
<div class="timer__controls">
|
||||
<button
|
||||
:variant="timer.isStarted ? 'outlined' : 'elevated'"
|
||||
class="btn btn--light btn--small"
|
||||
:class="{'is-active': true}"
|
||||
@click="toggleTimer"
|
||||
>
|
||||
{{ timer.isStarted ? 'Pause' : 'Start' }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn--light btn--icon"
|
||||
:disabled="!(timer.isStarted || timer.getTimeRemaining !== timer.getSessionTime * 60)"
|
||||
@click="clearTimer"
|
||||
>
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M21 12a9 9 0 0 1-9 9 9 9 0 0 1-.899-17.956c.495-.049.899.359.899.856s-.405.894-.898.956a7.2 7.2 0 1 0 4.948 1.19V7.5a.9.9 0 1 1-1.8 0V4a1 1 0 0 1 1-1h3.5a.9.9 0 1 1 0 1.8H17.4A8.99 8.99 0 0 1 21 12Z" fill="currentColor" /></svg>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn--light btn--icon"
|
||||
@click="timer.nextSession()"
|
||||
>
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3 4.503a1.5 1.5 0 0 1 2.367-1.224l10.5 7.444a1.5 1.5 0 0 1 .01 2.442l-10.5 7.556A1.5 1.5 0 0 1 3 19.505V4.502Zm18-.753a.75.75 0 0 0-.22-.53C20.64 3.079 20.2 3 20 3c-.199 0-.594.079-.735.22-.14.14-.265.331-.265.53v16.5c0 .199.143.39.284.53.14.141.517.22.716.22.199 0 .64-.079.78-.22a.75.75 0 0 0 .22-.53V3.75Z" fill="currentColor" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { onBeforeUnmount } from 'vue'
|
||||
import { useTimerStore } from '~/stores/timer'
|
||||
import { useTimerControls } from '~/composables/useTimerControls'
|
||||
|
||||
export default {
|
||||
name: 'PomodoroTimer',
|
||||
setup () {
|
||||
const timer = useTimerStore()
|
||||
const { toggleTimer, clearTimer } = useTimerControls()
|
||||
|
||||
const getMinutes = () => Math.floor(timer.getTimeRemaining / 60)
|
||||
const getSeconds = () => timer.getTimeRemaining - 60 * getMinutes()
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearTimer()
|
||||
})
|
||||
|
||||
return {
|
||||
timer,
|
||||
getMinutes,
|
||||
getSeconds,
|
||||
toggleTimer,
|
||||
clearTimer
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.timer {
|
||||
padding-top: 1em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
.timer__phases {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0.75em;
|
||||
}
|
||||
|
||||
.timer__clock {
|
||||
font-size: 3em;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
padding: 16px 0;
|
||||
position: relative;
|
||||
align-self: center;
|
||||
color: attr(color);
|
||||
|
||||
&::before {
|
||||
content: '#'attr(data-round);
|
||||
position: absolute;
|
||||
left: -3em;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #b1b1b1;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.timer__controls {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.timer__progress {
|
||||
width: 100%;
|
||||
background-color: white;
|
||||
|
||||
.progress {
|
||||
background-color: #72A5ED;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-focus {
|
||||
color: #72A5ED;
|
||||
}
|
||||
.is-short {
|
||||
color: #FBA9A9;
|
||||
}
|
||||
.is-long {
|
||||
color: #D77574;
|
||||
}
|
||||
|
||||
.tag {
|
||||
border-radius: 3px;
|
||||
padding: 0.25em 0.5em;
|
||||
font-weight: 700;
|
||||
font-size: 12px;
|
||||
flex: 1;
|
||||
|
||||
&.is-active {
|
||||
background-color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.btn--light {
|
||||
background-color: white;
|
||||
color: #585C5E;
|
||||
}
|
||||
|
||||
.btn--light:hover {
|
||||
background-color: #e9c046;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn--light:disabled {
|
||||
background-color: white;
|
||||
color: #585C5E;
|
||||
opacity: 0.4;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn--small {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.btn--light[variant="outlined"] {
|
||||
color: #e9c046;
|
||||
border-color: white;
|
||||
}
|
||||
.btn--light[variant="outlined"]:hover {
|
||||
background-color: white;
|
||||
border-color: white;
|
||||
color: #585C5E;
|
||||
}
|
||||
</style>
|
100
components/viz/DeepCareAnalyzer.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div class="progress" style="height: 10px">
|
||||
it works.
|
||||
<LineChart :chart-data="chartData" :options="chartOptions" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { Line } from 'vue-chartjs'
|
||||
import { Chart as ChartJS, Title, Tooltip, Legend, LineElement, CategoryScale, TimeScale, LinearScale } from 'chart.js'
|
||||
import Papa from 'papaparse'
|
||||
// Chart.js Komponenten registrieren
|
||||
ChartJS.register(Title, Tooltip, Legend, LineElement, CategoryScale, TimeScale, LinearScale)
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LineChart: Line // vue-chartjs Line-Komponente einbinden
|
||||
},
|
||||
setup () {
|
||||
// Reaktive Variablen für Daten und Chart-Optionen
|
||||
const chartData = ref(null)
|
||||
const chartOptions = ref({
|
||||
responsive: true,
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time', // Zeit als X-Achse
|
||||
time: {
|
||||
unit: 'min', // Zeitintervall auf der X-Achse (z.B. Minuten)
|
||||
tooltipFormat: 'll mm:ss:ms' // Format für den Tooltip
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Timestamp'
|
||||
}
|
||||
},
|
||||
y: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Noise Level'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// CSV-Daten laden und in das Chart-Format umwandeln
|
||||
const loadCSVData = async () => {
|
||||
// Beispiel CSV String (ersetzte es mit deinem tatsächlichen CSV)
|
||||
const csvString = `
|
||||
timestamp,sensors_ambientData_noise
|
||||
2024-10-09 08:00:00.169,35.19362197015299
|
||||
2024-10-09 08:00:00.369,34.89283749374632
|
||||
2024-10-09 08:00:00.569,34.54829716372913
|
||||
` // Hier deine CSV-Daten laden oder einen Upload implementieren
|
||||
|
||||
// CSV parsen mit PapaParse
|
||||
// eslint-disable-next-line import/no-named-as-default-member
|
||||
await Papa.parse(csvString, {
|
||||
header: true,
|
||||
dynamicTyping: true,
|
||||
complete: (result) => {
|
||||
// Extrahiere die Timestamps und Sensorwerte
|
||||
const labels = result.data.map(row => row.timestamp)
|
||||
const values = result.data.map(row => row.sensors_ambientData_noise)
|
||||
|
||||
// Erstelle die Chart-Datenstruktur
|
||||
chartData.value = {
|
||||
labels,
|
||||
datasets: [{
|
||||
label: 'Noise Level',
|
||||
data: values,
|
||||
borderColor: 'rgba(75, 192, 192, 1)',
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
||||
fill: false // Fülle den Bereich unter der Linie nicht
|
||||
}]
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// CSV-Daten laden, sobald die Komponente gemountet ist
|
||||
onMounted(() => {
|
||||
loadCSVData()
|
||||
})
|
||||
|
||||
return {
|
||||
chartData,
|
||||
chartOptions
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Optional: Styles für das Chart */
|
||||
canvas {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
</style>
|
71
components/viz/LineChart.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<div v-if="isLoaded">
|
||||
<div class="chart-container">
|
||||
<Line :data="chartData" :options="chartOptions" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
Loading chart data...
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, onMounted } from 'vue'
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend
|
||||
} from 'chart.js'
|
||||
import { Line } from 'vue-chartjs'
|
||||
import zoomPlugin from 'chartjs-plugin-zoom'
|
||||
import { data, options, loadChartData } from './dataExtractor'
|
||||
|
||||
// Register Chart.js components
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
zoomPlugin
|
||||
)
|
||||
|
||||
export default defineComponent({
|
||||
name: 'LineChart',
|
||||
components: {
|
||||
// eslint-disable-next-line vue/no-reserved-component-names
|
||||
Line
|
||||
},
|
||||
setup () {
|
||||
const chartData = ref(data)
|
||||
const chartOptions = ref(options)
|
||||
const isLoaded = ref(false)
|
||||
|
||||
// Load CSV data when the component is mounted
|
||||
onMounted(async () => {
|
||||
await loadChartData('/data_10_16_2024.csv')
|
||||
isLoaded.value = true
|
||||
})
|
||||
|
||||
return {
|
||||
chartData,
|
||||
chartOptions,
|
||||
isLoaded
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style scoped>
|
||||
.chart-container {
|
||||
position: relative;
|
||||
height: 90vh;
|
||||
width: 90vw;
|
||||
}
|
||||
</style>
|
79
components/viz/TimeDifference.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<div v-if="isLoaded">
|
||||
<div class="chart-container">
|
||||
<Line :data="chartData" :options="chartOptions" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
Loading chart data...
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, onMounted } from 'vue'
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend
|
||||
} from 'chart.js'
|
||||
import { Line } from 'vue-chartjs'
|
||||
import zoomPlugin from 'chartjs-plugin-zoom'
|
||||
import { data, options, loadChartData } from './dataExtractor'
|
||||
|
||||
// Register Chart.js components
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
zoomPlugin
|
||||
)
|
||||
|
||||
export default defineComponent({
|
||||
name: 'TimeDifference',
|
||||
components: {
|
||||
// eslint-disable-next-line vue/no-reserved-component-names
|
||||
Line
|
||||
},
|
||||
setup () {
|
||||
const chartData = ref(data)
|
||||
const chartOptions = ref(options)
|
||||
const isLoaded = ref(false)
|
||||
|
||||
function filterTimeDiffValues () {
|
||||
let timeDiffData = chartData.value.datasets[2].data as number[]
|
||||
// useNuxtApp().$logger.log('Length of timeDiffData before cleanup: ' + timeDiffData.keys.length)
|
||||
timeDiffData = timeDiffData.filter(val => val >= 1)
|
||||
// useNuxtApp().$logger.log('Length of timeDiffData after cleanup: ' + timeDiffData.keys.length)
|
||||
return timeDiffData
|
||||
}
|
||||
// Load CSV data when the component is mounted
|
||||
onMounted(async () => {
|
||||
await loadChartData('/data_10_16_2024.csv')
|
||||
chartData.value.datasets[2].data = filterTimeDiffValues()
|
||||
isLoaded.value = true
|
||||
})
|
||||
|
||||
return {
|
||||
chartData,
|
||||
chartOptions,
|
||||
isLoaded
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style scoped>
|
||||
.chart-container {
|
||||
position: relative;
|
||||
height: 90vh;
|
||||
width: 90vw;
|
||||
}
|
||||
</style>
|
60
components/viz/chartConfig.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
// Export data from CSV,
|
||||
|
||||
const data = {
|
||||
labels: [
|
||||
'16.10.24 09:50.123',
|
||||
'16.10.24 09:50.143',
|
||||
'16.10.24 09:50.166',
|
||||
'16.10.24 09:50.213',
|
||||
'16.10.24 09:50.199'
|
||||
],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Lautstärke (sensors_ambientData_noise)',
|
||||
backgroundColor: '#42a5f5',
|
||||
borderColor: '#1e88e5',
|
||||
data: [
|
||||
34.8861247388139, 34.8861247388139, 34.8861247388139, 38.0076076494897,
|
||||
38.0076076494897
|
||||
],
|
||||
fill: false
|
||||
},
|
||||
{
|
||||
label: 'Störfaktor (Mindboost Value)',
|
||||
backgroundColor: '#ffca28',
|
||||
borderColor: '#ff9800',
|
||||
data: [17.553421, 17.553421, 18.455361, 18.455361, 18.455361],
|
||||
fill: false
|
||||
},
|
||||
{
|
||||
label: 'time_diff',
|
||||
backgroundColor: '#fdctf',
|
||||
borderColor: '#ff9800',
|
||||
data: [143, 166, 213, 199, 214],
|
||||
fill: false
|
||||
}
|
||||
]
|
||||
}
|
||||
const options = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
x: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Timestamp'
|
||||
}
|
||||
},
|
||||
y: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Werte'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
export {
|
||||
// createMicrophoneSource,
|
||||
data,
|
||||
options
|
||||
}
|
16
components/viz/data/deepCareExtractor.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { extractAudioData } from './extractAudioData'
|
||||
|
||||
const labels: string[] = []
|
||||
const noise: number[] = []
|
||||
|
||||
// Funktion zum Laden der Daten und Initialisieren der Chart-Daten
|
||||
export const loadChartData = async (csvFilePath: string): Promise<void> => {
|
||||
const audioData = await extractAudioData(csvFilePath)
|
||||
|
||||
audioData.forEach((row:any) => {
|
||||
labels.push(row.timestamp)
|
||||
noise.push(row.sensors_ambientData_noise)
|
||||
})
|
||||
}
|
||||
|
||||
await loadChartData('/audioData.csv')
|
36
components/viz/data/extractAudioData.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import Papa from 'papaparse'
|
||||
|
||||
// Typdefinitionen für die Daten
|
||||
interface AudioData {
|
||||
timestamp: string
|
||||
sensors_ambientData_noise: number
|
||||
}
|
||||
|
||||
// Funktion zum Laden und Extrahieren der CSV-Daten
|
||||
export const extractAudioData = async (csvFilePath: string): Promise<AudioData[]> => {
|
||||
const response = await fetch(csvFilePath)
|
||||
const csvData = await response.text()
|
||||
|
||||
const audioData: AudioData[] = []
|
||||
|
||||
// eslint-disable-next-line import/no-named-as-default-member
|
||||
Papa.parse(csvData, {
|
||||
header: true,
|
||||
skipEmptyLines: true,
|
||||
complete: (result) => {
|
||||
result.data.forEach((row: any) => {
|
||||
// Extrahiere und parse die Daten
|
||||
const timestamp = row.timestamp
|
||||
const noise = parseFloat(row.sensors_ambientData_noise)
|
||||
|
||||
if (timestamp && !isNaN(noise)) {
|
||||
audioData.push({ timestamp, sensors_ambientData_noise: noise })
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => resolve(audioData), 0) // Simuliere async Verarbeitung
|
||||
})
|
||||
}
|
211
components/viz/dataExtractor.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import type { ChartData, ChartOptions } from 'chart.js'
|
||||
import Papa from 'papaparse'
|
||||
|
||||
// Initialize empty arrays for the chart data
|
||||
const labels: string[] = []
|
||||
const noise: number[] = []
|
||||
const disturb: number[] = []
|
||||
let timediff: number[] = []
|
||||
|
||||
// Funktion zum Formatieren des Sensorwerts
|
||||
function formatNoiseValue (rawValue: string):number {
|
||||
const firstFourDigits = rawValue.slice(0, 4) // Nimm die ersten 4 Zeichen
|
||||
const formattedValue = parseFloat(firstFourDigits.slice(0, 2) + '.' + firstFourDigits.slice(2)) // Füge Dezimalpunkt nach zwei Zeichen ein
|
||||
return formattedValue
|
||||
}
|
||||
|
||||
// to estimate the data quality a factor in % is calculated to figure out,
|
||||
// how much the data points differ from their 200ms timing
|
||||
export function filterTimeDiffValues (timeDiff:number[]) {
|
||||
// Filter the array to remove all differences below 1
|
||||
const filteredTimediff = timeDiff.filter(diff => diff >= 1)
|
||||
// Optionally, process the filtered data further
|
||||
return filteredTimediff
|
||||
}
|
||||
|
||||
// function smoothNoise (data: number[], windowSize: number): number[] {
|
||||
// const smoothed: number[] = []
|
||||
|
||||
// for (let i = 0; i < data.length; i++) {
|
||||
// const start = Math.max(0, i - Math.floor(windowSize / 2))
|
||||
// const end = Math.min(data.length, i + Math.floor(windowSize / 2) + 1)
|
||||
// const average =
|
||||
// data.slice(start, end).reduce((sum, value) => sum + value, 0) /
|
||||
// (end - start)
|
||||
// smoothed.push(average)
|
||||
// }
|
||||
|
||||
// return smoothed
|
||||
// };
|
||||
|
||||
// Fetch and parse the CSV file
|
||||
export const loadChartData = async (csvFilePath: string): Promise<void> => {
|
||||
const response = await fetch(csvFilePath)
|
||||
const csvData = await response.text()
|
||||
|
||||
// eslint-disable-next-line import/no-named-as-default-member
|
||||
Papa.parse(csvData, {
|
||||
header: true,
|
||||
skipEmptyLines: true,
|
||||
complete: (result) => {
|
||||
result.data.forEach((row: any) => {
|
||||
labels.push(row.timestamp)
|
||||
noise.push(formatNoiseValue(row.sensors_ambientData_noise.replace(/\./g, '')
|
||||
))
|
||||
disturb.push(parseFloat(row.disturbance_analysisData))
|
||||
timediff.push(parseFloat(row.time_diff || '0')) // Default to 0 if empty
|
||||
})
|
||||
const cleanTimeDiff: number[] = []
|
||||
timediff.forEach((diff) => {
|
||||
if (diff >= 1) { cleanTimeDiff.push(diff) }
|
||||
})
|
||||
timediff = cleanTimeDiff
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// function smoothNoise (data: number[], windowSize: number): number[] {
|
||||
// const smoothed: number[] = []
|
||||
|
||||
// for (let i = 0; i < data.length; i++) {
|
||||
// const start = Math.max(0, i - Math.floor(windowSize / 2))
|
||||
// const end = Math.min(data.length, i + Math.floor(windowSize / 2) + 1)
|
||||
// const average =
|
||||
// data.slice(start, end).reduce((sum, value) => sum + value, 0) /
|
||||
// (end - start)
|
||||
// smoothed.push(average)
|
||||
// }
|
||||
|
||||
// return smoothed
|
||||
// };
|
||||
|
||||
// Fetch and parse the CSV file
|
||||
export const loadCleanChartData = async (csvFilePath: string): Promise<void> => {
|
||||
const response = await fetch(csvFilePath)
|
||||
const csvData = await response.text()
|
||||
|
||||
// eslint-disable-next-line import/no-named-as-default-member
|
||||
Papa.parse(csvData, {
|
||||
header: true,
|
||||
skipEmptyLines: true,
|
||||
complete: (result) => {
|
||||
result.data.forEach((row: any) => {
|
||||
labels.push(row.timestamp)
|
||||
noise.push(formatNoiseValue(row.sensors_ambientData_noise.replace(/\./g, '')
|
||||
))
|
||||
disturb.push(parseFloat(row.disturbance_analysisData))
|
||||
timediff.push(parseFloat(row.time_diff || '0')) // Default to 0 if empty
|
||||
})
|
||||
const cleanTimeDiff: number[] = []
|
||||
timediff.forEach((diff) => {
|
||||
if (diff >= 1) { cleanTimeDiff.push(diff) }
|
||||
})
|
||||
timediff = cleanTimeDiff
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Export chart data and options (initially empty; will be filled after loading the CSV)
|
||||
export const data: ChartData<'line'> = {
|
||||
labels,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Lautstärke (Noise)',
|
||||
data: noise,
|
||||
borderColor: '#42a5f5',
|
||||
backgroundColor: '#42a5f5',
|
||||
fill: true,
|
||||
tension: 0.8,
|
||||
yAxisID: 'y-noise'
|
||||
},
|
||||
{
|
||||
label: 'Störfaktor (Disturbance)',
|
||||
data: disturb,
|
||||
borderColor: '#ff9800',
|
||||
backgroundColor: '#ff9800',
|
||||
fill: false,
|
||||
tension: 0.2,
|
||||
yAxisID: 'y-disturbance'
|
||||
},
|
||||
{
|
||||
label: 'time_diff',
|
||||
backgroundColor: '#fdctf',
|
||||
borderColor: '#C9C9C9',
|
||||
data: timediff,
|
||||
fill: false,
|
||||
tension: 1,
|
||||
yAxisID: 'y-timediff'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export const options: ChartOptions<'line'> = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
position: 'top'
|
||||
},
|
||||
zoom: {
|
||||
zoom: {
|
||||
wheel: {
|
||||
enabled: true // Enable zooming with the mouse wheel
|
||||
},
|
||||
pinch: {
|
||||
enabled: true // Enable zooming with touch gestures
|
||||
},
|
||||
mode: 'Tropics' // Allow zooming in both directions
|
||||
},
|
||||
pan: {
|
||||
enabled: true, // Enable panning
|
||||
mode: 'xy' // Allow panning in both directions
|
||||
}
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Lautstärke und Störfaktor'
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Timestamp'
|
||||
}
|
||||
},
|
||||
'y-noise': {
|
||||
type: 'linear',
|
||||
position: 'left', // Noise scale on the left
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Lautstärke (Geräuschkulisse)'
|
||||
},
|
||||
grid: {
|
||||
drawOnChartArea: true // Show grid lines for the noise Y-axis
|
||||
}
|
||||
},
|
||||
'y-disturbance': {
|
||||
type: 'linear',
|
||||
position: 'right', // Disturbance scale on the right
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Störfaktor (Disturbance)'
|
||||
},
|
||||
grid: {
|
||||
drawOnChartArea: false // Disable grid lines for the disturbance Y-axis
|
||||
}
|
||||
},
|
||||
'y-timediff': {
|
||||
type: 'linear',
|
||||
position: 'right', // Disturbance scale on the right
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Zeitunterschied'
|
||||
},
|
||||
grid: {
|
||||
drawOnChartArea: false // Disable grid lines for the disturbance Y-axis
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|