sounds, better files

This commit is contained in:
2024-08-18 19:11:41 +00:00
parent 9ed2f70701
commit 08489ed67b
86 changed files with 4226 additions and 86 deletions

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>