fixes and cleanup
This commit is contained in:
13
package-lock.json
generated
13
package-lock.json
generated
@@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"@types/chromecast-caf-receiver": "^6.0.16",
|
||||
"@types/chromecast-caf-sender": "^1.0.10",
|
||||
"uuid": "^11.0.3",
|
||||
"vite-plugin-list-directory-contents": "^1.4.5",
|
||||
"vue": "^3.4.29"
|
||||
},
|
||||
@@ -2526,6 +2527,18 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz",
|
||||
"integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"bin": {
|
||||
"uuid": "dist/esm/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/validator": {
|
||||
"version": "13.12.0",
|
||||
"resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"dependencies": {
|
||||
"@types/chromecast-caf-receiver": "^6.0.16",
|
||||
"@types/chromecast-caf-sender": "^1.0.10",
|
||||
"uuid": "^11.0.3",
|
||||
"vite-plugin-list-directory-contents": "^1.4.5",
|
||||
"vue": "^3.4.29"
|
||||
},
|
||||
|
||||
@@ -8,13 +8,9 @@ const chromecast = new ChromecastChannelCaster(searchParams.get('ws') ?? undefin
|
||||
const postMessage = new PostMessageChannel(window);
|
||||
chromecast.initialize();
|
||||
chromecast.on((msg) => {
|
||||
console.log('CHROMECAST MESSAGE', msg);
|
||||
console.log(msg);
|
||||
postMessage.send(msg);
|
||||
})
|
||||
postMessage.on((msg) => {
|
||||
console.log('POST MESSAGE', msg);
|
||||
console.log(msg);
|
||||
chromecast.send(msg);
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
@dragover.prevent="onDragover"
|
||||
>
|
||||
<div class="a-card__label-wrapper">
|
||||
<div class="a-card__label">{{ label }}</div>
|
||||
<input class="a-card__label" v-model="editableLabel" />
|
||||
<div class="a-card__delete" @click="emits('delete')">×</div>
|
||||
</div>
|
||||
<div class="a-card__volume">
|
||||
@@ -48,7 +48,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {ref, UnwrapNestedRefs} from 'vue';
|
||||
import {computed, ref, UnwrapNestedRefs} from 'vue';
|
||||
import {SoundGroupNode} from '../../model/Audio/SoundGroupNode';
|
||||
|
||||
import Play from '../Icons/Play.vue';
|
||||
@@ -82,6 +82,11 @@ const props = withDefaults(defineProps<{
|
||||
noDetail: false
|
||||
});
|
||||
|
||||
const editableLabel = computed({
|
||||
get: () => props.node.label,
|
||||
set: (value: string) => props.node.label = value
|
||||
});
|
||||
|
||||
const popover = ref<HTMLElement>();
|
||||
function openPopover() {
|
||||
if (props.noDetail) {
|
||||
@@ -92,17 +97,14 @@ function openPopover() {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
149
src/components/Audio/Parallel.vue
Normal file
149
src/components/Audio/Parallel.vue
Normal file
@@ -0,0 +1,149 @@
|
||||
<template>
|
||||
<AudioCard
|
||||
:node="node"
|
||||
:supports="[ 'togglePlay' ]"
|
||||
@delete="$emit('delete')"
|
||||
label="Parallel"
|
||||
style="--box-color: var(--color-emerald)"
|
||||
>
|
||||
<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%" />
|
||||
</div>
|
||||
</div>-->
|
||||
<div>
|
||||
<div
|
||||
class="a-parallel__song"
|
||||
:class="{
|
||||
'a-parallel__song--playing': song.isPlaying
|
||||
}"
|
||||
v-for="(song, idx) in node.sounds()"
|
||||
:key="song.options.vFile.name"
|
||||
@click="playTrack(song)"
|
||||
>
|
||||
<span class="a-parallel__song-play-indicator">
|
||||
<Play style="height: 1rem" />
|
||||
</span>
|
||||
<span class="a-parallel__song-name">
|
||||
{{ song.options.vFile.name }}
|
||||
</span>
|
||||
<span @click.prevent="removeSong(song)">
|
||||
×
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</AudioCard>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import AudioCard from './AudioCard.vue';
|
||||
import {ParallelGroupNode} from '../../model/Audio/ParallelGroupNode';
|
||||
import Play from '../Icons/Play.vue';
|
||||
import {MediaElementSoundNode} from '../../model/Audio/SoundNode';
|
||||
import {UnwrapNestedRefs} from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
node: UnwrapNestedRefs<ParallelGroupNode>
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'delete': [],
|
||||
}>();
|
||||
|
||||
// playlist.connect(audioContext.destination);
|
||||
|
||||
function playTrack(song: MediaElementSoundNode) {
|
||||
props.node.toggleTrack(song);
|
||||
}
|
||||
|
||||
function removeSong(song: MediaElementSoundNode) {
|
||||
props.node.removeSoundNode(song);
|
||||
}
|
||||
|
||||
</script>
|
||||
<style>
|
||||
|
||||
@layer component {
|
||||
.a-parallel {
|
||||
--box-color: var(--audio-box-color-parallel, #10b981);
|
||||
--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-parallel input {
|
||||
accent-color: var(--box-color);
|
||||
}
|
||||
|
||||
.a-parallel__top, .a-parallel__bottom {
|
||||
height: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.a-parallel__content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;;
|
||||
border-radius: 4px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.a-parallel__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);
|
||||
}
|
||||
}
|
||||
|
||||
.a-parallel__song {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
border-bottom: 1px solid var(--box-color-800);
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.a-parallel__song-name {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.a-parallel__song--playing {
|
||||
background-color: var(--box-color-800);
|
||||
}
|
||||
|
||||
.a-parallel__song:first-child {
|
||||
border-top: 1px solid var(--box-color-800);
|
||||
}
|
||||
|
||||
.a-parallel__song-play-indicator {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.a-parallel__song:hover .a-parallel__song-play-indicator,
|
||||
.a-parallel__song--playing .a-parallel__song-play-indicator {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -20,6 +20,11 @@
|
||||
:node="node"
|
||||
@delete="deleteNode(node)"
|
||||
/>
|
||||
<Parallel
|
||||
v-else-if="node.type === 'parallel'"
|
||||
:node="node"
|
||||
@delete="deleteNode(node)"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<div class="a-card add-card" style="--box-color: var(--color-zinc)">
|
||||
@@ -32,6 +37,9 @@
|
||||
<div @click="addNode('sfx')" class="soundboard-add-card__btn">
|
||||
Sfx
|
||||
</div>
|
||||
<div @click="addNode('parallel')" class="soundboard-add-card__btn">
|
||||
Parallel
|
||||
</div>
|
||||
<div @click="addNode('tab')" class="soundboard-add-card__btn">
|
||||
Tab Source
|
||||
</div>
|
||||
@@ -50,6 +58,8 @@ import TabSource from './TabSource.vue';
|
||||
import DeviceVolume from './DeviceVolume.vue';
|
||||
import {AudioNodeManager} from '../../model/Audio/AudioNodesManager';
|
||||
import {debounce} from '../../utils/debounce';
|
||||
import {ParallelGroupNode} from '../../model/Audio/ParallelGroupNode';
|
||||
import Parallel from './Parallel.vue';
|
||||
|
||||
const fs = inject(MountedFsSymbol)!;
|
||||
const audioContext = inject(AudioContextSymbol) as AudioContext;
|
||||
@@ -61,29 +71,43 @@ const nodes = reactive(audioNodesManager.restore({fs, audioContext}));
|
||||
function addNode(name: string) {
|
||||
if (name === 'playlist') {
|
||||
nodes.push(reactive(new PlaylistGroupNode({
|
||||
currentTrack: 0,
|
||||
currentTrackTime: 0,
|
||||
volume: 100,
|
||||
isMuted: false,
|
||||
sounds: [],
|
||||
label: 'Playlist',
|
||||
currentTrack: 0,
|
||||
currentTrackTime: 0,
|
||||
volume: 100,
|
||||
isMuted: false,
|
||||
sounds: [],
|
||||
}, {
|
||||
fs,
|
||||
audioContext,
|
||||
})))
|
||||
} else if (name === 'sfx') {
|
||||
nodes.push(reactive(new SfxGroupNode({
|
||||
isMuted: false,
|
||||
volume: 100,
|
||||
sounds: [],
|
||||
label: 'SFX',
|
||||
isMuted: false,
|
||||
volume: 100,
|
||||
sounds: [],
|
||||
}, {
|
||||
fs,
|
||||
audioContext,
|
||||
})))
|
||||
} else if (name === 'tab') {
|
||||
nodes.push(reactive(new TabSourceNode({
|
||||
isMuted: false,
|
||||
volume: 100,
|
||||
sounds: [],
|
||||
label: 'Tab Source',
|
||||
isMuted: false,
|
||||
volume: 100,
|
||||
sounds: [],
|
||||
}, {
|
||||
fs,
|
||||
audioContext,
|
||||
})))
|
||||
} else if (name === 'parallel') {
|
||||
nodes.push(reactive(new ParallelGroupNode({
|
||||
label: 'Parallel',
|
||||
isMuted: false,
|
||||
volume: 100,
|
||||
enabledTracks: [],
|
||||
sounds: [],
|
||||
}, {
|
||||
fs,
|
||||
audioContext,
|
||||
@@ -115,10 +139,8 @@ const dest = audioContext.createGain();
|
||||
|
||||
|
||||
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})
|
||||
|
||||
|
||||
@@ -35,7 +35,6 @@ function onClose() {
|
||||
|
||||
watch(() => props.open, (open) => {
|
||||
if (open) {
|
||||
console.log(dialog.value);
|
||||
dialog.value?.showModal();
|
||||
popover.value?.showPopover();
|
||||
} else {
|
||||
|
||||
@@ -69,7 +69,6 @@ function onFilePreview(file: VirtualFile) {
|
||||
|
||||
watch(search, async (value) => {
|
||||
searchResults.value = [];
|
||||
console.log('search', value, value.length);
|
||||
if (value.length < 3) {
|
||||
return;
|
||||
}
|
||||
@@ -117,16 +116,6 @@ async function buildIndex() {
|
||||
grid-gap: 4px;
|
||||
}
|
||||
|
||||
.file-manager__preview {
|
||||
max-height: 25vh;
|
||||
transition: max-height 200ms;
|
||||
transition-behavior: allow-discrete;
|
||||
}
|
||||
|
||||
.file-manager__preview:hover {
|
||||
max-height: initial;
|
||||
}
|
||||
|
||||
.file-manager-dir {
|
||||
list-style-type: none;
|
||||
padding-left: 1rem;
|
||||
|
||||
@@ -59,8 +59,12 @@ async function toggleCollapse() {
|
||||
async function fetchDirectoryContent() {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
// preload from cache
|
||||
content.value = await fs.listDir(props.dir, {cache: 'only-cache'});
|
||||
try {
|
||||
// preload from cache
|
||||
content.value = await fs.listDir(props.dir, {cache: 'only-cache'});
|
||||
} catch (err) {
|
||||
// ignore missing cache
|
||||
}
|
||||
// replace with network
|
||||
content.value = await fs.listDir(props.dir, {cache: 'use-network'});
|
||||
} finally {
|
||||
|
||||
@@ -1,27 +1,37 @@
|
||||
<template>
|
||||
<div v-if="currentPreviewUrl" class="file-preview">
|
||||
<img
|
||||
v-if="currentPrevewType?.startsWith('image/')"
|
||||
<div class="file-preview" ref="root">
|
||||
<template v-if="currentPreviewUrl && visible">
|
||||
<img
|
||||
v-if="currentPrevewType?.startsWith('image/')"
|
||||
:src="currentPreviewUrl"
|
||||
alt="preview"
|
||||
class="file-preview__image" />
|
||||
<video
|
||||
v-if="currentPrevewType?.startsWith('video/')"
|
||||
:src="currentPreviewUrl"
|
||||
alt="preview"
|
||||
class="file-preview__image" />
|
||||
<video
|
||||
v-if="currentPrevewType?.startsWith('video/')"
|
||||
:src="currentPreviewUrl"
|
||||
autoplay
|
||||
muted
|
||||
loop
|
||||
playsinline
|
||||
class="file-preview__video"
|
||||
></video>
|
||||
autoplay
|
||||
muted
|
||||
loop
|
||||
playsinline
|
||||
class="file-preview__video"
|
||||
></video>
|
||||
<audio
|
||||
v-if="currentPrevewType?.startsWith('audio/')"
|
||||
autoplay
|
||||
:src="currentPreviewUrl"
|
||||
class="file-preview__audio"
|
||||
controls
|
||||
:muted="muted"
|
||||
@volumechange="muted = ($event.target as any).muted"
|
||||
></audio>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {inject, ref, watch} from 'vue';
|
||||
import {inject, onMounted, ref, watch} from 'vue';
|
||||
import {VirtualFile} from '../../model/FileSystem/types';
|
||||
import {MountedFsSymbol} from '../../model/injectionSymbols';
|
||||
|
||||
|
||||
const props=defineProps<{
|
||||
file?:VirtualFile
|
||||
}>()
|
||||
@@ -30,10 +40,12 @@ const fs = inject(MountedFsSymbol)!;
|
||||
const currentPreviewUrl = ref<string | null>(null);
|
||||
const currentPrevewType = ref<string | null>(null);
|
||||
let abort: AbortController | undefined = undefined;
|
||||
const root = ref<HTMLElement>();
|
||||
const visible = ref(false);
|
||||
const muted = ref(false);
|
||||
|
||||
|
||||
watch(()=> props.file, async (file)=>{
|
||||
console.log('file', file);
|
||||
if (abort) {
|
||||
abort.abort('User canceled');
|
||||
}
|
||||
@@ -43,29 +55,45 @@ watch(()=> props.file, async (file)=>{
|
||||
}
|
||||
currentPreviewUrl.value = null;
|
||||
try {
|
||||
console.log('downloading thumbnail');
|
||||
// @ts-ignore
|
||||
const blob = await fs.downloadThumbnail(file, {abortSignal: abort.signal});
|
||||
currentPreviewUrl.value = URL.createObjectURL(blob);
|
||||
// @ts-ignore
|
||||
currentPrevewType.value = file.mimeType;
|
||||
console.log('currentPreviewUrl', currentPreviewUrl.value);
|
||||
abort = undefined;
|
||||
} catch (e) {
|
||||
if (e !== 'User canceled') {
|
||||
console.error(e);
|
||||
} else {
|
||||
console.log('User canceled');
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
new IntersectionObserver((entries) => {
|
||||
if (entries[0].isIntersecting) {
|
||||
visible.value = true;
|
||||
} else {
|
||||
visible.value = false;
|
||||
}
|
||||
}).observe(root.value!);
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
<style>
|
||||
|
||||
.file-preview {
|
||||
width: 100%;
|
||||
height: 30vh;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.file-preview__audio,
|
||||
.file-preview__video,
|
||||
.file-preview__image {
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
</style>
|
||||
@@ -58,7 +58,6 @@ let canvasDisplay: undefined | CanvasDisplay;
|
||||
function selectScreen (screen: any) {
|
||||
if (screen.imageSink?.display) {
|
||||
selectedScreen.value = screen;
|
||||
console.log('selectScreen!!!', screen.imageSink?.display);
|
||||
canvasDisplay?.disconnectTarget(canvas.value!);
|
||||
canvasDisplay = screen.imageSink?.display;
|
||||
canvasDisplay?.connectTarget(canvas.value!);
|
||||
@@ -66,7 +65,6 @@ function selectScreen (screen: any) {
|
||||
}
|
||||
|
||||
async function onDrop(file: VirtualFile) {
|
||||
console.log('onDrop!!', file);
|
||||
onLoad(file);
|
||||
}
|
||||
|
||||
@@ -77,7 +75,6 @@ const dragging = ref(false);
|
||||
|
||||
function onScroll(event: WheelEvent) {
|
||||
event.preventDefault();
|
||||
// console.log('scroll', event.deltaY, event.offsetX, event.offsetY);
|
||||
const delta = -1 * event.deltaY / 2000;
|
||||
if (!canvasDisplay) {
|
||||
return;
|
||||
|
||||
@@ -41,7 +41,6 @@ function redraw() {
|
||||
ctx: map.value!.getContext('2d')!,
|
||||
...mapContext.value,
|
||||
};
|
||||
console.log('redraw', ctx.scale);
|
||||
ctx.ctx.fillStyle = 'black';
|
||||
ctx.ctx.fillRect(0, 0, ctx.virtualWidth, ctx.virtualHeight);
|
||||
for (const layer of props.layers) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
export async function loadImage(src: string | File): Promise<HTMLImageElement> {
|
||||
if (src instanceof File) {
|
||||
console.log('Creating object URL');
|
||||
src = URL.createObjectURL(src);
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -43,7 +43,6 @@ export class DistanceMeasurement implements CanvasDisplayDrawable {
|
||||
ctx.scale(1/display.scale, 1/display.scale);
|
||||
ctx.translate(display.offsetX - display.width/2, display.offsetY - display.height / 2 );
|
||||
if (this.pointer) {
|
||||
console.log('Drawing pointer', );
|
||||
|
||||
ctx.fillStyle = 'rgba(255,0,0,0.5)';
|
||||
ctx.beginPath();
|
||||
@@ -65,7 +64,9 @@ export class DistanceMeasurement implements CanvasDisplayDrawable {
|
||||
return;
|
||||
}
|
||||
const lengthPx = Math.sqrt(Math.pow(this.start!.x - this.end!.x, 2) + Math.pow(this.start!.y - this.end!.y, 2));
|
||||
const chebychevPx = Math.max(Math.abs(this.start!.x - this.end!.x), Math.abs(this.start!.y - this.end!.y));
|
||||
const lengthFeet = ((lengthPx / display.dpi) * 5) / display.scale;
|
||||
const chebychevFeet = Math.round(((chebychevPx / display.dpi) / display.scale)) * 5;
|
||||
const roundedFeets = Math.floor(lengthFeet/5) * 5;
|
||||
const roundedPx = ((roundedFeets) / 5) * display.dpi * display.scale;
|
||||
const halfInchInPx = 0.5 * display.dpi * display.scale;
|
||||
@@ -93,7 +94,7 @@ export class DistanceMeasurement implements CanvasDisplayDrawable {
|
||||
ctx.fillRect(this.end!.x, this.end!.y - 30, 200, 40);
|
||||
ctx.font = "30px Arial";
|
||||
ctx.fillStyle = 'red';
|
||||
ctx.fillText(`${roundedFeets} / ${lengthFeet.toFixed(2)} ft`, this.end!.x + 5, this.end!.y);
|
||||
ctx.fillText(`${chebychevFeet} / ${lengthFeet.toFixed(2)} ft`, this.end!.x + 5, this.end!.y);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,7 +132,9 @@ export class MapLayerDrawable implements CanvasDisplayDrawable {
|
||||
}
|
||||
if (this.file.type.startsWith('image/')) {
|
||||
this.img = await loadImage(this.file);
|
||||
this.mapWidthInInches = this.mapWidth ?? 10;
|
||||
if (this.mapWidth) {
|
||||
this.mapWidthInInches = this.mapWidth;
|
||||
}
|
||||
if (this.img.naturalWidth < this.img.naturalHeight) {
|
||||
this.rotation = 1;
|
||||
}
|
||||
@@ -146,6 +149,9 @@ export class MapLayerDrawable implements CanvasDisplayDrawable {
|
||||
video.loop = true;
|
||||
video.play();
|
||||
await videoMeta;
|
||||
if (this.mapWidth) {
|
||||
this.mapWidthInInches = this.mapWidth;
|
||||
}
|
||||
if (video.videoWidth < video.videoHeight) {
|
||||
this.rotation = 1;
|
||||
}
|
||||
@@ -169,6 +175,7 @@ export class MapLayerDrawable implements CanvasDisplayDrawable {
|
||||
private setDefaultDPI(display: CanvasDisplay, width: number, height: number) {
|
||||
if (this.dpi === undefined) {
|
||||
this.dpi = Math.min((width / display.width), height / display.height) * display.dpi;
|
||||
this.mapWidth = Math.floor(width / this.dpi);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,7 +287,6 @@ export class CanvasDisplay {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
if (diameter) {
|
||||
console.log('Setting diameter', diameter);
|
||||
this.setDiameter(diameter);
|
||||
}
|
||||
this.offsetX = 0;
|
||||
@@ -364,7 +370,6 @@ export class CanvasDisplay {
|
||||
this.targets.add(target);
|
||||
if (this.targets.size > 0) {
|
||||
this.start();
|
||||
console.log('Canvas', this.canvas);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,7 +379,6 @@ export class CanvasDisplay {
|
||||
|
||||
async getStream() {
|
||||
this.redrawBuffer();
|
||||
console.log('Streamed Canvas', this.canvas.width, this.canvas.height);
|
||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
return this.canvas.captureStream(25);
|
||||
}
|
||||
|
||||
@@ -53,7 +53,6 @@ import {DeviceConfig} from '../../model/Remote/RemoteDeviceManager';
|
||||
|
||||
const name = ref('');
|
||||
const manager = inject(RemoteDeviceManagerSymbol)!;
|
||||
console.log(manager);
|
||||
const localWindowDialog = ref<HTMLDialogElement>();
|
||||
|
||||
const availableDevices = ref<DeviceConfig[]>([]);
|
||||
|
||||
@@ -55,7 +55,6 @@ const {onMouseDown} = useMouseDragging()
|
||||
|
||||
function onWheel(event: WheelEvent) {
|
||||
scale.value = Math.max(10, Math.min(1000, scale.value + event.deltaY / 100));
|
||||
console.log('scroll', event.deltaY, scale.value);
|
||||
}
|
||||
|
||||
|
||||
@@ -71,7 +70,6 @@ async function loadFile(meta: VirtualFile, file: File) {
|
||||
const map = new MapLayerDrawable(file, meta?.map?.width);
|
||||
await map.preloadFile();
|
||||
imageSink.value.display.mapLayer = map
|
||||
console.log(imageSink.value?.display);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,8 +93,6 @@ async function onDrop(event: DragEvent) {
|
||||
await loadFile(meta, file);
|
||||
}
|
||||
}
|
||||
console.log('files', event.dataTransfer?.files);
|
||||
console.log('drop', event.dataTransfer?.getData('application/json'));
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
@@ -8,6 +8,5 @@ import {reactive} from 'vue';
|
||||
import {SoundPlayer} from '../model/SoundPlayer';
|
||||
|
||||
const soundPlayer = reactive(new SoundPlayer());
|
||||
console.log(soundPlayer);
|
||||
|
||||
</script>
|
||||
@@ -26,8 +26,6 @@ const slots = defineSlots<{
|
||||
Id: string,
|
||||
}>()
|
||||
|
||||
console.log(slots)
|
||||
|
||||
const selectedTab = ref(props.tabs?.[0]?.key);
|
||||
|
||||
</script>
|
||||
|
||||
@@ -12,7 +12,6 @@ const app = createApp(ControlApp);
|
||||
|
||||
const chromecastChannelCaster = new ChromecastChannelCaster();
|
||||
chromecastChannelCaster.initialize();
|
||||
console.log('chromecastChannelCaster', chromecastChannelCaster);
|
||||
|
||||
const audioContext = new AudioContext();
|
||||
|
||||
@@ -44,6 +43,13 @@ app
|
||||
key: '426pBJgSyfmgJAaUmNjk',
|
||||
share: 'nCKcVyl',
|
||||
},
|
||||
{
|
||||
name: 'Scenes',
|
||||
type: 'filestash',
|
||||
domain: 'https://files.fisoft.eu',
|
||||
key: '426pBJgSyfmgJAaUmNjk',
|
||||
share: 'ONiRCBi',
|
||||
},
|
||||
{
|
||||
name: 'Audio',
|
||||
type: 'filestash',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ParallelGroupNode } from "./ParallelGroupNode";
|
||||
import { PlaylistGroupNode } from "./PlaylistGroupNode";
|
||||
import { SfxGroupNode } from "./SfxGroupNode";
|
||||
import { SoundGroupNode } from "./SoundGroupNode";
|
||||
@@ -6,7 +7,7 @@ import { SoundboardContext } from "./types";
|
||||
|
||||
|
||||
type SerializedNode<T extends SoundGroupNode<any>> = ReturnType<T['serializeOptions']>
|
||||
export type SupportedDefinitions = SerializedNode<PlaylistGroupNode> | SerializedNode<SfxGroupNode> | SerializedNode<TabSourceNode>
|
||||
export type SupportedDefinitions = SerializedNode<PlaylistGroupNode> | SerializedNode<SfxGroupNode> | SerializedNode<TabSourceNode> | SerializedNode<ParallelGroupNode>;
|
||||
|
||||
|
||||
export class AudioNodeManager {
|
||||
@@ -23,6 +24,8 @@ export class AudioNodeManager {
|
||||
return new SfxGroupNode(definition, ctx);
|
||||
} else if (definition.type === 'tabSource') {
|
||||
return new TabSourceNode(definition, ctx);
|
||||
} else if (definition.type === 'parallel') {
|
||||
return new ParallelGroupNode(definition, ctx);
|
||||
} else {
|
||||
throw new Error('Invalid node type');
|
||||
}
|
||||
|
||||
75
src/model/Audio/ParallelGroupNode.ts
Normal file
75
src/model/Audio/ParallelGroupNode.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { VirtualFile } from "../FileSystem/types";
|
||||
import { SoundGroupNode, SoundGroupNodeOptions } from "./SoundGroupNode";
|
||||
import { MediaElementSoundNode } from "./SoundNode";
|
||||
|
||||
export interface ParallelGroupNodeOptions extends SoundGroupNodeOptions {
|
||||
enabledTracks: string[];
|
||||
}
|
||||
|
||||
export class ParallelGroupNode extends SoundGroupNode<ParallelGroupNodeOptions, MediaElementSoundNode> {
|
||||
|
||||
public readonly type = 'parallel';
|
||||
private enabledTracks: Set<string> = new Set();
|
||||
|
||||
get isPlaying() {
|
||||
return this.soundNodes.some(sound => sound.isPlaying);
|
||||
}
|
||||
|
||||
play() {
|
||||
this.soundNodes.forEach((sound, i) => {
|
||||
if (this.enabledTracks.has(sound.id)) {
|
||||
sound.play();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
removeSoundNode(node: MediaElementSoundNode) {
|
||||
this.enabledTracks.delete(node.id);
|
||||
super.removeSoundNode(node);
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.soundNodes.forEach(sound => sound.stop());
|
||||
}
|
||||
|
||||
togglePlay() {
|
||||
if (this.isPlaying) {
|
||||
this.stop();
|
||||
} else {
|
||||
this.play();
|
||||
}
|
||||
}
|
||||
|
||||
deserializeOptions(options: ParallelGroupNodeOptions) {
|
||||
this.enabledTracks = new Set(options.enabledTracks);
|
||||
super.deserializeOptions(options);
|
||||
}
|
||||
|
||||
toggleTrack(track: MediaElementSoundNode) {
|
||||
if (this.enabledTracks.has(track.id)) {
|
||||
track.pause();
|
||||
this.enabledTracks.delete(track.id);
|
||||
} else {
|
||||
this.enabledTracks.add(track.id);
|
||||
track.play();
|
||||
}
|
||||
}
|
||||
|
||||
serializeOptions() {
|
||||
return {
|
||||
...super.serializeOptions(),
|
||||
volume: this.volume,
|
||||
isMuted: this.options.isMuted,
|
||||
enabledTracks: [...this.options.enabledTracks.values()] as string[],
|
||||
type: this.type,
|
||||
} as const;
|
||||
}
|
||||
|
||||
addSound(vFile: VirtualFile) {
|
||||
this.addSoundNode({
|
||||
type: 'mediaElement',
|
||||
vFile,
|
||||
volume: 100,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { BufferedSoundNode, MediaElementSoundNode, PartiaSoundNodeOptions, Sound
|
||||
import { SoundboardContext } from "./types";
|
||||
|
||||
export interface SoundGroupNodeOptions {
|
||||
label: string,
|
||||
volume: number,
|
||||
isMuted: boolean,
|
||||
sounds: PartiaSoundNodeOptions[]
|
||||
@@ -29,25 +30,35 @@ export class DeviceSource {
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class SoundGroupNode<T extends SoundGroupNodeOptions> {
|
||||
export abstract class SoundGroupNode<T extends SoundGroupNodeOptions, TNode extends SoundNode=SoundNode> {
|
||||
|
||||
public abstract readonly type: string;
|
||||
public deviceSources: Map<RemoteDevice, DeviceSource> = new Map();
|
||||
// @ts-ignore it's in deserializeOptions
|
||||
protected options: Omit<T, 'sounds'>
|
||||
protected gainNode: GainNode;
|
||||
protected soundNodes: SoundNode[] = [];
|
||||
protected soundNodes: TNode[] = [];
|
||||
|
||||
constructor(
|
||||
options: T,
|
||||
protected ctx: SoundboardContext
|
||||
) {
|
||||
this.options = options;
|
||||
this.deserializeOptions(options);
|
||||
this.gainNode = this.ctx.audioContext.createGain();
|
||||
this.gainNode.gain.value = (options.volume / 100);
|
||||
for (const sound of options.sounds) {
|
||||
this.addSoundNode(sound);
|
||||
}
|
||||
}
|
||||
|
||||
get label() {
|
||||
return this.options.label;
|
||||
}
|
||||
|
||||
set label(val: string) {
|
||||
this.options.label = val;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
addSound(vFile: VirtualFile) {
|
||||
// implement in subclass
|
||||
@@ -56,16 +67,18 @@ export abstract class SoundGroupNode<T extends SoundGroupNodeOptions> {
|
||||
addSoundNode(options: PartiaSoundNodeOptions) {
|
||||
if (options.type === 'buffered') {
|
||||
const sound = new BufferedSoundNode(options, this.ctx);
|
||||
// @ts-ignore
|
||||
this.soundNodes.push(sound);
|
||||
sound.connect(this.gainNode);
|
||||
} else if (options.type === 'mediaElement') {
|
||||
const sound = new MediaElementSoundNode(options, this.ctx);
|
||||
// @ts-ignore
|
||||
this.soundNodes.push(sound);
|
||||
sound.connect(this.gainNode);
|
||||
}
|
||||
}
|
||||
|
||||
removeSoundNode(node: SoundNode) {
|
||||
removeSoundNode(node: TNode) {
|
||||
node?.stop();
|
||||
const index = this.soundNodes.indexOf(node);
|
||||
if (index > -1) {
|
||||
@@ -86,7 +99,6 @@ export abstract class SoundGroupNode<T extends SoundGroupNodeOptions> {
|
||||
|
||||
|
||||
connect(destination: AudioNode) {
|
||||
console.log('connecting', this, destination);
|
||||
this.gainNode.connect(destination);
|
||||
}
|
||||
|
||||
@@ -140,6 +152,10 @@ export abstract class SoundGroupNode<T extends SoundGroupNodeOptions> {
|
||||
return this.deviceSources.get(remoteDevice)!;
|
||||
}
|
||||
|
||||
deserializeOptions(options: T) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
serializeOptions() {
|
||||
return {
|
||||
...this.options,
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
import { MountedFs } from "../FileSystem/MountedFs";
|
||||
import { VirtualFile } from "../FileSystem/types";
|
||||
import { SoundboardContext } from "./types";
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
|
||||
export interface PartiaSoundNodeOptions {
|
||||
id?: string;
|
||||
vFile: VirtualFile;
|
||||
volume?: number;
|
||||
type: 'buffered' | 'mediaElement';
|
||||
}
|
||||
|
||||
export interface SoundNodeOptions extends PartiaSoundNodeOptions {
|
||||
id: string;
|
||||
volume: number;
|
||||
}
|
||||
|
||||
export abstract class SoundNode {
|
||||
export abstract class SoundNode<T extends SoundNodeOptions = SoundNodeOptions> {
|
||||
|
||||
public readonly id: string;
|
||||
protected gainNode: GainNode;
|
||||
protected file?: File;
|
||||
public options: SoundNodeOptions;
|
||||
public options: T;
|
||||
public readonly numberOfInputs = 0;
|
||||
public readonly numberOfOutputs = 1;
|
||||
protected activeSources: AudioBufferSourceNode[] = [];
|
||||
@@ -29,10 +33,14 @@ export abstract class SoundNode {
|
||||
options: PartiaSoundNodeOptions,
|
||||
ctx: SoundboardContext,
|
||||
) {
|
||||
|
||||
// @ts-ignore
|
||||
this.options = {
|
||||
id: uuidv4(),
|
||||
...options,
|
||||
volume: options.volume ?? 100
|
||||
}
|
||||
this.id = options.id!;
|
||||
this.audioContext = ctx.audioContext;
|
||||
this.fs = ctx.fs;
|
||||
this.gainNode = this.audioContext.createGain();
|
||||
@@ -58,7 +66,6 @@ export abstract class SoundNode {
|
||||
}
|
||||
|
||||
connect(destination: AudioNode) {
|
||||
console.log('connecting', this, destination);
|
||||
this.gainNode.connect(destination);
|
||||
}
|
||||
|
||||
@@ -72,6 +79,7 @@ export abstract class SoundNode {
|
||||
|
||||
setVolume(volume: number) {
|
||||
this.gainNode.gain.value = volume / 100;
|
||||
this.options.volume = volume;
|
||||
}
|
||||
|
||||
getVolume() {
|
||||
@@ -115,7 +123,11 @@ export class BufferedSoundNode extends SoundNode {
|
||||
}
|
||||
}
|
||||
|
||||
export class MediaElementSoundNode extends SoundNode {
|
||||
export interface MediaElementSoundNodeOptions extends SoundNodeOptions {
|
||||
loop: boolean;
|
||||
}
|
||||
|
||||
export class MediaElementSoundNode extends SoundNode<MediaElementSoundNodeOptions> {
|
||||
private mediaElement?: HTMLMediaElement;
|
||||
private source?: MediaElementAudioSourceNode;
|
||||
private _isPlaying = false;
|
||||
@@ -124,6 +136,9 @@ export class MediaElementSoundNode extends SoundNode {
|
||||
await super.preload();
|
||||
if (!this.mediaElement) {
|
||||
this.mediaElement = document.createElement('audio');
|
||||
if (this.options.loop) {
|
||||
this.mediaElement.loop = true;
|
||||
}
|
||||
this.mediaElement.src = URL.createObjectURL(this.file!);
|
||||
this.source = this.audioContext.createMediaElementSource(this.mediaElement);
|
||||
this.source.connect(this.gainNode);
|
||||
@@ -132,7 +147,6 @@ export class MediaElementSoundNode extends SoundNode {
|
||||
this._isPlaying = false;
|
||||
});
|
||||
this.mediaElement.addEventListener('playing', (event) => {
|
||||
console.log(event);
|
||||
this._isPlaying = true;
|
||||
});
|
||||
this.mediaElement.addEventListener('pause', () => {
|
||||
|
||||
@@ -38,7 +38,6 @@ export class TabSourceNode extends SoundGroupNode<TabSourceNodeOptions> {
|
||||
surfaceSwitching: "include",
|
||||
monitorTypeSurfaces: "include",
|
||||
});
|
||||
console.log(this.stream);
|
||||
this.source = this.ctx.audioContext.createMediaStreamSource(this.stream);
|
||||
this.source.connect(this.gainNode);
|
||||
} catch (err) {
|
||||
|
||||
@@ -128,12 +128,10 @@ export class WebRtcChannelSummon extends MessageEmitter implements JsonChannelSu
|
||||
event.streams.forEach((stream) => {
|
||||
stream.getTracks().forEach((track) => {
|
||||
if (event.track.kind === 'audio') {
|
||||
console.log('audio track', track);
|
||||
const audioElement = document.createElement('audio');
|
||||
audioElement.srcObject = stream; // Attach the received stream
|
||||
audioElement.play();
|
||||
} else if (event.track.kind === 'video') {
|
||||
console.log('video track', track);
|
||||
const videoEl = document.getElementById('mainVideo') as HTMLVideoElement;
|
||||
if (videoEl) {
|
||||
videoEl.srcObject = stream;
|
||||
|
||||
@@ -47,7 +47,6 @@ export class RemoteDeviceManager {
|
||||
}
|
||||
|
||||
async add(device: DeviceConfig, connect: boolean = false): Promise<void> {
|
||||
console.log('add', device);
|
||||
const instance = await this.createInstance(device);
|
||||
if (!instance) {
|
||||
return;
|
||||
|
||||
@@ -16,10 +16,7 @@ export class SoundPlayer {
|
||||
const randomOption = options[Math.floor(Math.random() * options.length)];
|
||||
const soundBuffer = await this.fetchSound(sound, randomOption);
|
||||
this.playBuffer(soundBuffer);
|
||||
console.log(`Playing sound: ${sound}`);
|
||||
} else {
|
||||
console.log(`Sound not found: ${sound}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fetchSoundOptions(sound: string) {
|
||||
@@ -40,7 +37,6 @@ export class SoundPlayer {
|
||||
if (this.cache.has(url)) {
|
||||
return this.cache.get(url)!;
|
||||
}
|
||||
console.log(`Fetching sound: ${sound}/${options}`);
|
||||
const arrayBuffer = await (await fetch(url)).arrayBuffer();
|
||||
const audioBuffer = await this.ctx.decodeAudioData(arrayBuffer);
|
||||
this.cache.set(url, audioBuffer);
|
||||
|
||||
@@ -108,7 +108,6 @@ const dynamicStyle = computed(() => {
|
||||
|
||||
|
||||
function onVideoLoaded(event: Event) {
|
||||
console.log('video loaded', event.target);
|
||||
mediaDimensions.value = {
|
||||
width: video.value!.videoWidth,
|
||||
height: video.value!.videoHeight,
|
||||
|
||||
@@ -13,7 +13,6 @@ import { JsonChannelSymbol, WebRtcChannelSymbol } from '../../model/injectionSym
|
||||
|
||||
const jsonMessages = ref<{source: 'json' | 'webrtc', message: Message}[]>([]);
|
||||
const lastMessages = computed(() => [...jsonMessages.value].reverse().slice(0, 10));
|
||||
console.log(jsonMessages, lastMessages);
|
||||
const channel = inject(JsonChannelSymbol)!;
|
||||
const webRTC = inject(WebRtcChannelSymbol)!;
|
||||
channel.on((message) => {
|
||||
|
||||
Reference in New Issue
Block a user