sounds, better files
This commit is contained in:
237
src/components/Audio/AudioCard.vue
Normal file
237
src/components/Audio/AudioCard.vue
Normal file
@@ -0,0 +1,237 @@
|
||||
<template>
|
||||
<div
|
||||
class="a-card"
|
||||
:class="{ 'a-card--playing': node.isPlaying }"
|
||||
@drop.prevent="onDrop"
|
||||
@dragover.prevent="onDragover"
|
||||
>
|
||||
<div class="a-card__label-wrapper">
|
||||
<div class="a-card__label">{{ label }}</div>
|
||||
</div>
|
||||
<div class="a-card__volume">
|
||||
<span style="height: 1.25rem" @click="node.toggleMute()">
|
||||
<Mute v-if="node.isMuted" style="height: 1.25rem" />
|
||||
<Speaker v-else style="height: 1.25rem" />
|
||||
</span>
|
||||
<input type="range" min="0" max="100" v-model="node.volume" style="width: 100%;" />
|
||||
</div>
|
||||
|
||||
<div class="a-card__content" @click="openPopover">
|
||||
<slot name="preview"></slot>
|
||||
</div>
|
||||
|
||||
<div class="a-card__controls">
|
||||
<Backward v-if="supports?.includes('prev')" style="height: 1rem;" @click="$emit('prev')" />
|
||||
<div class="a-card__play-button"
|
||||
v-if="supports?.includes('togglePlay') || (supports?.includes('stop') && (node.isPlaying || supports?.includes('play')))"
|
||||
>
|
||||
<template v-if="supports?.includes('togglePlay')">
|
||||
<Play v-if="!node.isPlaying" style="height: 1.5rem" @click="node.togglePlay()" />
|
||||
<Pause v-if="node.isPlaying" style="height: 1.5rem" @click="node.togglePlay()" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<Play v-if="!node.isPlaying && supports?.includes('play')" style="height: 1.5rem" @click="node.togglePlay()" />
|
||||
<Stop v-if="node.isPlaying && supports?.includes('stop')" style="height: 1.5rem" @click="node.togglePlay()" />
|
||||
</template>
|
||||
</div>
|
||||
<Forward v-if="supports?.includes('next')" style="height: 1rem" @click="$emit('next')" />
|
||||
</div>
|
||||
|
||||
<div class="a-card__popover" popover ref="popover">
|
||||
<DeviceVolume
|
||||
class="a-card__popover-volumes"
|
||||
:deviceSource="(device) => node.getDeviceSource(device).gainNode"
|
||||
/>
|
||||
<slot name="popover"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {mergeProps, ref} from 'vue';
|
||||
import {SoundGroupNode} from '../../model/Audio/SoundGroupNode';
|
||||
|
||||
import Play from '../Icons/Play.vue';
|
||||
import Backward from '../Icons/Backward.vue';
|
||||
import Forward from '../Icons/Forward.vue';
|
||||
import Mute from '../Icons/Mute.vue';
|
||||
import Speaker from '../Icons/Speaker.vue';
|
||||
import Pause from '../Icons/Pause.vue';
|
||||
import DeviceVolume from './DeviceVolume.vue';
|
||||
import Stop from '../Icons/Stop.vue';
|
||||
|
||||
const emits = defineEmits<{
|
||||
'next': [],
|
||||
'prev': [],
|
||||
'togglePlay': [],
|
||||
'update:label': [string]
|
||||
}>();
|
||||
|
||||
type Supports = 'next' | 'prev' | 'togglePlay' | 'stop' | 'play';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
node: SoundGroupNode<any>
|
||||
supports?: Supports[]
|
||||
label?: string,
|
||||
noDetail?: boolean
|
||||
}>(), {
|
||||
// @ts-ignore
|
||||
supports: [],
|
||||
label: 'Audio',
|
||||
noDetail: false
|
||||
});
|
||||
|
||||
const popover = ref<HTMLElement>();
|
||||
function openPopover() {
|
||||
if (props.noDetail) {
|
||||
return;
|
||||
}
|
||||
popover.value?.showPopover();
|
||||
}
|
||||
|
||||
function onDragover(event: DragEvent) {
|
||||
const data = event.dataTransfer?.getData('application/json');
|
||||
console.log('dragover', event, '>', data, '<', typeof data);
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
function onDrop(event: DragEvent) {
|
||||
console.log('drop', event, event.dataTransfer?.getData('application/json'));
|
||||
event.preventDefault();
|
||||
if (event.dataTransfer?.getData('application/json')) {
|
||||
const meta = JSON.parse(event.dataTransfer?.getData('application/json'));
|
||||
if (meta.type === 'file') {
|
||||
console.log('got file', meta);
|
||||
props.node.addSound(meta);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
|
||||
@layer component {
|
||||
|
||||
@property --beat-color {
|
||||
syntax: "<color>";
|
||||
inherits: true;
|
||||
initial-value: transparent;
|
||||
}
|
||||
|
||||
.a-card {
|
||||
--box-color: var(--audio-box-color-playlist, #ffb308);
|
||||
--box-color-900: color-mix(in lab, var(--box-color), #000 80%);
|
||||
--box-color-100: color-mix(in lab, var(--box-color), #fff 80%);
|
||||
--box-color-300: color-mix(in lab, var(--box-color), #fff 40%);
|
||||
width: 160px;
|
||||
aspect-ratio: 4/5;
|
||||
border: 1px solid rgba(from var(--box-color) r g b / 0.15);
|
||||
padding: 4px;
|
||||
background: linear-gradient(45deg, var(--box-color-900), color-mix(in lab, var(--box-color-900), #000 60%));
|
||||
box-shadow: inset 10px -10px 30px 5px rgba(from var(--box-color-300) r g b / 0.05);
|
||||
color: var(--box-color-100);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
--beat-color: rgba(from var(--box-color) r g b / 0);
|
||||
transition: --beat-color linear 1s;
|
||||
box-shadow:
|
||||
inset 10px -10px 30px 5px rgba(from var(--box-color-300) r g b / 0.05),
|
||||
0 0 var(--beat-color),
|
||||
0 0 0 8px var(--beat-color),
|
||||
0 0 0 16px var(--beat-color);
|
||||
}
|
||||
|
||||
.a-card--playing {
|
||||
--beat-color: rgba(from var(--box-color) r g b / 0.3);/*color-mix(in lab, var(--box-color), #000 60%);*/
|
||||
animation: ripples 1s linear infinite;
|
||||
}
|
||||
|
||||
.a-card__controls {
|
||||
height: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width:100%;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.a-card__play-button {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
margin-left: 0.5rem;
|
||||
margin-right: 0.5rem;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: var(--box-color);
|
||||
color: var(--box-color-900);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.a-card input {
|
||||
accent-color: var(--box-color);
|
||||
}
|
||||
|
||||
.a-card__volume {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 0rem 0.5rem
|
||||
}
|
||||
|
||||
.a-card__label-wrapper {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin-top: -1rem;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
.a-card__label {
|
||||
display: inline-block;
|
||||
width: 80%;
|
||||
text-align: center;
|
||||
border: 1px solid rgba(from var(--box-color) r g b / 0.2);
|
||||
background: var(--box-color-900);
|
||||
color: var(--box-color-100);
|
||||
font-size: 1rem;
|
||||
font-weight: semibold;
|
||||
padding: 0.125rem;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.a-card__content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;;
|
||||
border-radius: 4px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.a-card__popover {
|
||||
height: calc(100% - 10rem);
|
||||
width: calc(100% - 10rem);
|
||||
background: var(--box-color-900);
|
||||
color: var(--box-color-100);
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
border: 2px solid rgba(from var(--box-color) r g b / 0.2);
|
||||
background: linear-gradient(45deg, var(--box-color-900), color-mix(in lab, var(--box-color-900), #000 60%));
|
||||
box-shadow: inset 10px -10px 30px 5px rgba(from var(--box-color-300) r g b / 0.05);
|
||||
}
|
||||
|
||||
.a-card__popover-volumes {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes ripples {
|
||||
to {
|
||||
box-shadow:
|
||||
inset 10px -10px 30px 5px rgba(from var(--box-color-300) r g b / 0.05),
|
||||
0 0 0 8px var(--beat-color),
|
||||
0 0 0 16px var(--beat-color),
|
||||
0 0 0 32px rgba(from var(--beat-color) r g b / 0);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
78
src/components/Audio/DeviceVolume.vue
Normal file
78
src/components/Audio/DeviceVolume.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<div class="device-volumes">
|
||||
<div
|
||||
v-for="{device, audioSink} in devicesWithSink"
|
||||
class="device-sink theme contrast frosted"
|
||||
:class="{
|
||||
'device-sink--chromecast': device.type === 'chromecast',
|
||||
'device-sink--second-screen': device.type === 'fullscreen-window',
|
||||
'device-sink--local': device.type === 'local',
|
||||
}"
|
||||
>
|
||||
<span>
|
||||
{{ device.name }}
|
||||
</span>
|
||||
<input type="range" min="0" max="1" step="0.01" v-model="audioSink.gain.value">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {computed, inject} from 'vue';
|
||||
import {RemoteDeviceManagerSymbol} from '../../model/injectionSymbols';
|
||||
import {RemoteDevice} from '../../model/Remote/types';
|
||||
import {LocalAudioDevice} from '../../model/Remote/LocalAudioDevice';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
deviceSource?: (device: RemoteDevice | LocalAudioDevice) => GainNode
|
||||
}>(), {
|
||||
deviceSource: (device: RemoteDevice | LocalAudioDevice) => device.audioSink
|
||||
});
|
||||
|
||||
const devicesWithSink = computed(() => {
|
||||
return deviceManager.audioDevices.map((device) => ({
|
||||
device,
|
||||
audioSink: props.deviceSource(device)
|
||||
}));
|
||||
});
|
||||
|
||||
const deviceManager = inject(RemoteDeviceManagerSymbol)!
|
||||
</script>
|
||||
<style>
|
||||
|
||||
.device-volumes {
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.device-sink {
|
||||
--color-theme: var(--device-color);
|
||||
display: inline-flex;
|
||||
padding: var(--s-2);
|
||||
border-radius: var(--border-radius);
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--s-2);
|
||||
}
|
||||
|
||||
.device-sink--chromecast {
|
||||
--device-color: var(--color-sky);
|
||||
}
|
||||
|
||||
.device-sink--second-screen {
|
||||
--device-color: var(--color-emerald);
|
||||
}
|
||||
|
||||
.device-sink--local {
|
||||
--device-color: var(--color-rose);
|
||||
}
|
||||
|
||||
.device-sink input {
|
||||
accent-color: var(--device-color);
|
||||
}
|
||||
|
||||
</style>
|
||||
95
src/components/Audio/Playlist.vue
Normal file
95
src/components/Audio/Playlist.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<AudioCard
|
||||
:node="node"
|
||||
:supports="[ 'next', 'prev', 'togglePlay' ]"
|
||||
@next="node.next()"
|
||||
@prev="node.prev()"
|
||||
label="Playlist test"
|
||||
style="--box-color: var(--color-amber)"
|
||||
>
|
||||
<template #preview>
|
||||
<div>{{ node.currentTrack?.name ?? 'No track selected' }}</div>
|
||||
</template>
|
||||
|
||||
<template #popover>
|
||||
<div>
|
||||
{{ node.currentTrack?.name ?? 'No track selected' }}
|
||||
<div>
|
||||
<input type="range" min="0" max="100" style="width: 100%" />00:00
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="a-playlist__song"
|
||||
v-for="(song, idx) in node.sounds()"
|
||||
:key="song.options.vFile.name"
|
||||
@click="playTrack(idx)"
|
||||
>
|
||||
<span>{{ song.options.vFile.name }}</span>
|
||||
<span>00:00</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</AudioCard>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import AudioCard from './AudioCard.vue';
|
||||
import {PlaylistGroupNode} from '../../model/Audio/PlaylistGroupNode';
|
||||
|
||||
const props = defineProps<{
|
||||
node: PlaylistGroupNode
|
||||
}>();
|
||||
|
||||
// playlist.connect(audioContext.destination);
|
||||
|
||||
function playTrack(idx: number) {
|
||||
props.node.changeTrack(idx);
|
||||
}
|
||||
|
||||
</script>
|
||||
<style>
|
||||
|
||||
@layer component {
|
||||
.a-playlist {
|
||||
--box-color: var(--audio-box-color-playlist, #f59e0b);
|
||||
--box-color-900: color-mix(in lab, var(--box-color), #000 80%);
|
||||
--box-color-100: color-mix(in lab, var(--box-color), #fff 80%);
|
||||
--box-color-300: color-mix(in lab, var(--box-color), #fff 40%);
|
||||
width: 160px;
|
||||
aspect-ratio: 4/5;
|
||||
border: 2px solid rgba(from var(--box-color) r g b / 0.2);
|
||||
padding: 4px;
|
||||
background: var(--box-color-900);
|
||||
color: var(--box-color-100);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.a-playlist input {
|
||||
accent-color: var(--box-color);
|
||||
}
|
||||
|
||||
.a-playlist__top, .a-playlist__bottom {
|
||||
height: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.a-playlist__content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;;
|
||||
border-radius: 4px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.a-playlist__popover {
|
||||
height: calc(100% - 10rem);
|
||||
width: calc(100% - 10rem);
|
||||
background: var(--box-color-900);
|
||||
color: var(--box-color-100);
|
||||
border-radius: 4px;
|
||||
border: 2px solid var(--box-color-300);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
58
src/components/Audio/Sfx.vue
Normal file
58
src/components/Audio/Sfx.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<AudioCard
|
||||
:style="{
|
||||
'--box-color': 'var(--color-fuchsia)'
|
||||
}"
|
||||
:supports="['stop']"
|
||||
label="SFX"
|
||||
:node="node"
|
||||
>
|
||||
<template #preview>
|
||||
<div
|
||||
v-for="sound in node.activeSounds"
|
||||
>
|
||||
{{ sound.name }}
|
||||
</div>
|
||||
</template>
|
||||
<template #popover>
|
||||
<div class="a-sfx__popover">
|
||||
<SfxSound
|
||||
v-for="[name, sounds] in grouppedSounds"
|
||||
:key="name"
|
||||
:sounds="sounds"
|
||||
:name="name"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</AudioCard>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {computed} from 'vue';
|
||||
import {SfxGroupNode} from '../../model/Audio/SfxGroupNode';
|
||||
import AudioCard from './AudioCard.vue';
|
||||
import {groupBy} from '../../utils/groupBy';
|
||||
import SfxSound from './SfxSound.vue';
|
||||
|
||||
|
||||
const props = defineProps<{
|
||||
node: SfxGroupNode,
|
||||
}>();
|
||||
|
||||
const grouppedSounds = computed(() => {
|
||||
return groupBy(props.node.sounds(), (sound) => {
|
||||
return sound.name.replace(/(\d+)$/, '').trim();
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
<style>
|
||||
.a-sfx__popover {
|
||||
--box-size: 160px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(var(--box-size), 1fr));
|
||||
gap: var(--s-5)
|
||||
}
|
||||
|
||||
</style>
|
||||
129
src/components/Audio/SfxSound.vue
Normal file
129
src/components/Audio/SfxSound.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<div
|
||||
class="theme a-sfx-sound contrast frosted"
|
||||
:class="{ 'a-sfx-sound--playing': isPlaying }"
|
||||
@click="playRandomSound(sounds)"
|
||||
>
|
||||
<div class="a-sfx-sound__controls">
|
||||
<Stop style="height: 2rem" v-if="isPlaying" @click.stop="stop" />
|
||||
</div>
|
||||
<div class="a-sfx-sound__name" >
|
||||
{{ name }}
|
||||
</div>
|
||||
|
||||
<template v-if="sounds.length > 1">
|
||||
<div class="a-sfx-sound__alternatives">
|
||||
<span
|
||||
v-for="sound in sounds"
|
||||
:key="sound.name"
|
||||
class="a-sfx-sound__alternative"
|
||||
@click.stop="sound.play()"
|
||||
>
|
||||
{{ getSoundNumber(sound) }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {computed} from 'vue';
|
||||
import {SoundNode} from '../../model/Audio/SoundNode';
|
||||
import Stop from '../Icons/Stop.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
sounds: SoundNode[],
|
||||
name: string,
|
||||
}>();
|
||||
|
||||
const getSoundNumber = (sound: SoundNode) => {
|
||||
const match = sound.name.match(/(\d+)$/);
|
||||
return match ? parseInt(match[1]) : 0;
|
||||
}
|
||||
|
||||
|
||||
const playRandomSound = (sounds: SoundNode[]) => {
|
||||
const sound = sounds[Math.floor(Math.random() * sounds.length)];
|
||||
sound.play();
|
||||
}
|
||||
|
||||
const isPlaying = computed(() => {
|
||||
return props.sounds.some(sound => sound.isPlaying);
|
||||
});
|
||||
|
||||
const stop = () => {
|
||||
props.sounds.forEach(sound => sound.stop());
|
||||
}
|
||||
|
||||
</script>
|
||||
<style>
|
||||
|
||||
.a-sfx-sound {
|
||||
width: 100%;
|
||||
height: var(--box-size);
|
||||
--color-theme: var(--box-color);
|
||||
position: relative;
|
||||
/* aspect-ratio: 1/1; */
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
border-radius: var(--border-radius);
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
|
||||
}
|
||||
|
||||
.a-sfx-sound__controls {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.a-sfx-sound__alternatives {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
color: var(--color-contrast-700);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--s-0_5);
|
||||
padding: var(--s-0_5);
|
||||
|
||||
}
|
||||
|
||||
.a-sfx-sound__alternative {
|
||||
flex: 1;
|
||||
padding: 0.5rem;
|
||||
text-align: center;
|
||||
color: var(--color-contrast-700);
|
||||
border: 1px solid var(--color-contrast-50);
|
||||
background-color: rgba(from var(--color-theme-950) r g b / 0.4);
|
||||
border-radius: var(--border-radius-sm);
|
||||
}
|
||||
|
||||
.a-sfx-sound__name {
|
||||
text-align: center;
|
||||
color: var(--color-contrast-900);
|
||||
font-weight: 400;
|
||||
padding: var(--s-1) var(--s-2);
|
||||
}
|
||||
|
||||
|
||||
.a-sfx-sound {
|
||||
--beat-color: rgba(from var(--box-color) r g b / 0);
|
||||
transition: --beat-color linear 1s;
|
||||
box-shadow:
|
||||
inset 10px -10px 30px 5px rgba(from var(--box-color-300) r g b / 0.05),
|
||||
0 0 var(--beat-color),
|
||||
0 0 0 8px var(--beat-color),
|
||||
0 0 0 16px var(--beat-color);
|
||||
}
|
||||
|
||||
.a-sfx-sound--playing {
|
||||
--beat-color: rgba(from var(--box-color) r g b / 0.3);/*color-mix(in lab, var(--box-color), #000 60%);*/
|
||||
animation: ripples 1s linear infinite;
|
||||
}
|
||||
|
||||
</style>
|
||||
120
src/components/Audio/Soundboard.vue
Normal file
120
src/components/Audio/Soundboard.vue
Normal file
@@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<div class="soundboard">
|
||||
<div class="soundboard__controls">
|
||||
<DeviceVolume />
|
||||
</div>
|
||||
<div class="soundboard__sounds" style="--color-theme: var(--color-lime)">
|
||||
<template v-for="node in nodes" :key="node">
|
||||
<Playlist
|
||||
v-if="node.type === 'playlist'"
|
||||
:node="node"
|
||||
/>
|
||||
<Sfx
|
||||
v-else-if="node.type === 'sfx'"
|
||||
:node="node"
|
||||
/>
|
||||
<TabSource
|
||||
v-else-if="node.type === 'tabSource'"
|
||||
:node="node"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {inject, reactive, ref, watch} from 'vue';
|
||||
import Playlist from './Playlist.vue';
|
||||
import {PlaylistGroupNode} from '../../model/Audio/PlaylistGroupNode';
|
||||
import {AudioContextSymbol, MountedFsSymbol, RemoteDeviceManagerSymbol} from '../../model/injectionSymbols';
|
||||
import {SfxGroupNode} from '../../model/Audio/SfxGroupNode';
|
||||
import Sfx from './Sfx.vue';
|
||||
import Modal from './modal.vue';
|
||||
import {TabSourceNode} from '../../model/Audio/TabSourceNode';
|
||||
import TabSource from './TabSource.vue';
|
||||
import DeviceVolume from './DeviceVolume.vue';
|
||||
import {AudioNodeManager} from '../../model/Audio/AudioNodesManager';
|
||||
import {debounce} from '../../utils/debounce';
|
||||
|
||||
const fs = inject(MountedFsSymbol)!;
|
||||
const audioContext = inject(AudioContextSymbol) as AudioContext;
|
||||
const deviceManager = inject(RemoteDeviceManagerSymbol)!;
|
||||
|
||||
const nodesDefinition = JSON.parse(localStorage.getItem('soundboard') ?? '[]');
|
||||
const audioNodesManager = new AudioNodeManager();
|
||||
|
||||
// const playlist = reactive(new PlaylistGroupNode({
|
||||
// currentTrack: 0,
|
||||
// currentTrackTime: 0,
|
||||
// volume: 100,
|
||||
// isMuted: false,
|
||||
// sounds: [],
|
||||
// }, {
|
||||
// fs,
|
||||
// audioContext,
|
||||
// }))
|
||||
|
||||
// const sfx = reactive(new SfxGroupNode({
|
||||
// isMuted: false,
|
||||
// volume: 100,
|
||||
// sounds: [],
|
||||
// }, {
|
||||
// fs,
|
||||
// audioContext,
|
||||
// }))
|
||||
|
||||
// const tabSource = reactive(new TabSourceNode({
|
||||
// isMuted: false,
|
||||
// volume: 100,
|
||||
// sounds: [],
|
||||
// }, {
|
||||
// fs,
|
||||
// audioContext,
|
||||
// }))
|
||||
|
||||
const nodes = reactive(audioNodesManager.restore({fs, audioContext}));
|
||||
|
||||
watch(() => nodes, debounce((newNodes) => {
|
||||
console.log('NEW_NODES', newNodes);
|
||||
audioNodesManager.store(newNodes);
|
||||
}, 500), {
|
||||
deep: true,
|
||||
})
|
||||
|
||||
const dest = audioContext.createGain();
|
||||
|
||||
// for (const node of nodes) {
|
||||
// // node.connect(audioContext.destination);
|
||||
// node.connect(dest);
|
||||
// }
|
||||
|
||||
|
||||
watch(() => deviceManager.audioDevices.length, () => {
|
||||
console.log('DEVICE_MANAGER', deviceManager)
|
||||
for(const device of deviceManager.audioDevices) {
|
||||
dest.connect(device.audioSink);
|
||||
console.log('DEVICES', device, device.audioSink);
|
||||
}
|
||||
}, {immediate: true})
|
||||
|
||||
</script>
|
||||
<style>
|
||||
.soundboard {
|
||||
|
||||
}
|
||||
|
||||
|
||||
.soundboard__controls {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 0 1rem 2rem 1rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.soundboard__sounds {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||
gap: 3rem 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
21
src/components/Audio/TabSource.vue
Normal file
21
src/components/Audio/TabSource.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
|
||||
<template>
|
||||
<AudioCard
|
||||
:style="{
|
||||
'--box-color': 'var(--color-red)'
|
||||
}"
|
||||
:supports="['play', 'stop']"
|
||||
label="Tab Audio"
|
||||
:node="node"
|
||||
>
|
||||
</AudioCard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {TabSourceNode} from '../../model/Audio/TabSourceNode';
|
||||
import AudioCard from './AudioCard.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
node: TabSourceNode,
|
||||
}>();
|
||||
</script>
|
||||
69
src/components/Audio/modal.vue
Normal file
69
src/components/Audio/modal.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<dialog class="dialog" ref="dialog" @close="onClose" @keydown.esc="onCancel">
|
||||
<div class="dialog-main">
|
||||
<slot></slot>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam ac ante mollis, fermentum nunc nec, ultricies nunc. Donec
|
||||
</div>
|
||||
<div class="dialog-aside">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam ac ante mollis, fermentum nunc nec, ultricies nunc. Donec
|
||||
</div>
|
||||
</dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
const dialog = ref<HTMLDialogElement>();
|
||||
const popover = ref<HTMLDivElement>();
|
||||
const props = defineProps<{
|
||||
open: boolean,
|
||||
preventClose: boolean,
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:open': [boolean]
|
||||
}>();
|
||||
|
||||
function onCancel(event: Event) {
|
||||
if (props.preventClose) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
function onClose(event: Event) {
|
||||
emit('update:open', false);
|
||||
}
|
||||
|
||||
watch(() => props.open, (open) => {
|
||||
if (open) {
|
||||
console.log(dialog.value);
|
||||
dialog.value?.showModal();
|
||||
popover.value?.showPopover();
|
||||
} else {
|
||||
dialog.value?.close();
|
||||
}
|
||||
}, {immediate: true});
|
||||
|
||||
</script>
|
||||
<style>
|
||||
.dialog[open] {
|
||||
display: grid;
|
||||
max-width: 800px;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: 4rem;
|
||||
background: none;
|
||||
border: none;
|
||||
pointer-events: visible;
|
||||
}
|
||||
|
||||
.dialog[open]::backdrop {
|
||||
background: rgba(from red r g b / 0.5);
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
.dialog-main, .dialog-aside {
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
border: 5px solid black;
|
||||
outline: none;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user