Explorar el Código

Implement serialization and deserialization for nodes and soundscapes

master
Fen Dweller hace 4 años
padre
commit
c9cd0f99b9
Se han modificado 13 ficheros con 346 adiciones y 63 borrados
  1. +16
    -2
      src/audio.ts
  2. +4
    -5
      src/components/Draggable.vue
  3. +13
    -5
      src/components/Menu.vue
  4. +36
    -20
      src/components/SoundscapeComp.vue
  5. +17
    -4
      src/components/nodes/FilterNode.vue
  6. +1
    -1
      src/components/nodes/SourceNode.vue
  7. +63
    -0
      src/data/presets.ts
  8. +0
    -15
      src/data/sound-sets.ts
  9. +18
    -0
      src/main.ts
  10. +150
    -0
      src/serialize.ts
  11. +0
    -2
      src/sources/IntervalSource.ts
  12. +14
    -1
      src/sources/LoopingSource.ts
  13. +14
    -8
      src/sources/Source.ts

+ 16
- 2
src/audio.ts Ver fichero

@@ -61,7 +61,9 @@ export class Soundscape {
this.filterBus.connect(this.output); this.filterBus.connect(this.output);
} }
} }

export abstract class Node { export abstract class Node {
abstract kind: string;
constructor(public name: string) {} constructor(public name: string) {}
} }


@@ -85,6 +87,8 @@ export type RangeMetadata = PropMetadata & {
unmap?: (value: number) => number; unmap?: (value: number) => number;
}; };


export type SoundSetMetadata = PropMetadata;

export const exposedMetadataNumber = Symbol("exposedNumber"); export const exposedMetadataNumber = Symbol("exposedNumber");


// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
@@ -99,6 +103,13 @@ export function exposedRange(options: RangeMetadata) {
return Reflect.metadata(exposedRangeMetadata, options); return Reflect.metadata(exposedRangeMetadata, options);
} }


export const exposedSoundSetMetadata = Symbol("exposedSoundSet");

// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function exposedSoundSet(options: SoundSetMetadata) {
return Reflect.metadata(exposedSoundSetMetadata, options);
}

export let context: AudioContext; export let context: AudioContext;


const audioBaseUrl = "/audio/"; const audioBaseUrl = "/audio/";
@@ -255,19 +266,22 @@ export function clearCache(): void {
// if the indexedDB table doesn't exist at all, make it // if the indexedDB table doesn't exist at all, make it
function createCache(): void { function createCache(): void {
const idb = window.indexedDB; const idb = window.indexedDB;

const req = idb.open("cache", 1); const req = idb.open("cache", 1);


console.log("Create cache");
req.onupgradeneeded = (event) => { req.onupgradeneeded = (event) => {
const db = req.result; const db = req.result;


if (event.oldVersion > 0 && event.oldVersion < 3) {
console.log("Version change");
if (event.oldVersion > 0) {
db.deleteObjectStore("audio"); db.deleteObjectStore("audio");
} }


db.createObjectStore("audio", { keyPath: ["name"] }); db.createObjectStore("audio", { keyPath: ["name"] });
}; };


console.log(req);

req.onerror = () => { req.onerror = () => {
alert("Couldn't open the database?"); alert("Couldn't open the database?");
}; };


+ 4
- 5
src/components/Draggable.vue Ver fichero

@@ -1,6 +1,6 @@
<template> <template>
<div class="draggable" draggable="true" v-on:dragstart="dragstart"> <div class="draggable" draggable="true" v-on:dragstart="dragstart">
<div class="label">{{ label }}</div>
<div class="label">{{ node.name }}</div>
</div> </div>
</template> </template>


@@ -9,15 +9,14 @@ import { Options, Vue } from "vue-class-component";


@Options({ @Options({
props: { props: {
label: String,
node: { name: String, kind: String },
}, },
}) })
export default class Draggable extends Vue { export default class Draggable extends Vue {
label!: string;
node!: { name: string; kind: string };


dragstart(event: DragEvent): void { dragstart(event: DragEvent): void {
console.log(event?.dataTransfer);
event?.dataTransfer?.setData("text/plain", this.label);
event?.dataTransfer?.setData(this.node.kind, JSON.stringify(this.node));
} }
} }
</script> </script>


+ 13
- 5
src/components/Menu.vue Ver fichero

@@ -3,9 +3,17 @@
<div class="list-label">Sounds</div> <div class="list-label">Sounds</div>
<div class="list"> <div class="list">
<draggable <draggable
v-for="(source, index) in soundSets"
v-for="(source, index) in presetSources"
:key="index" :key="index"
:label="source.name"
:node="source"
/>
</div>
<div class="list-label">Filters</div>
<div class="list">
<draggable
v-for="(source, index) in presetFilters"
:key="index"
:node="source"
/> />
</div> </div>
</div> </div>
@@ -14,8 +22,7 @@
<script lang="ts"> <script lang="ts">
import { Options, Vue } from "vue-class-component"; import { Options, Vue } from "vue-class-component";
import Draggable from "@/components/Draggable.vue"; import Draggable from "@/components/Draggable.vue";
import * as SoundSets from "@/data/sound-sets";
import { SoundSet } from "@/sources/Source";
import { PresetSources, PresetFilters } from "@/data/presets";


@Options({ @Options({
components: { components: {
@@ -23,7 +30,8 @@ import { SoundSet } from "@/sources/Source";
}, },
}) })
export default class Menu extends Vue { export default class Menu extends Vue {
soundSets: Array<SoundSet> = Array.from(Object.values(SoundSets));
presetSources: Array<{ name: string; kind: "Source" }> = PresetSources;
presetFilters: Array<{ name: string }> = PresetFilters;
} }
</script> </script>




+ 36
- 20
src/components/SoundscapeComp.vue Ver fichero

@@ -8,8 +8,8 @@
> >
</source-node> </source-node>
<source-node <source-node
v-on:drop="drop"
v-on:dragover="drag"
v-on:drop="dropSource"
v-on:dragover="dragSource"
:dummy="true" :dummy="true"
></source-node> ></source-node>
<filter-node <filter-node
@@ -18,6 +18,11 @@
:filter="filter" :filter="filter"
> >
</filter-node> </filter-node>
<filter-node
v-on:drop="dropFilter"
v-on:dragover="dragFilter"
:dummy="true"
></filter-node>
</div> </div>
<div></div> <div></div>
</template> </template>
@@ -27,10 +32,9 @@ import { Soundscape } from "@/audio";
import { Options, Vue } from "vue-class-component"; import { Options, Vue } from "vue-class-component";
import SourceNode from "./nodes/SourceNode.vue"; import SourceNode from "./nodes/SourceNode.vue";
import FilterNode from "./nodes/FilterNode.vue"; import FilterNode from "./nodes/FilterNode.vue";
import * as SoundSets from "@/data/sound-sets";
import { SoundSet, Source } from "@/sources/Source";
import { IntervalSource } from "@/sources/IntervalSource";
import { LoopingSource } from "@/sources/LoopingSource";
import { Source } from "@/sources/Source";
import { deserializeNode } from "@/serialize";
import { Filter } from "@/filters/Filter";


@Options({ @Options({
props: { props: {
@@ -45,28 +49,38 @@ export default class SoundscapeComp extends Vue {
soundscape!: Soundscape; soundscape!: Soundscape;
started = false; started = false;
context!: AudioContext; context!: AudioContext;
sources: { [key: string]: new (name: string) => Source } = {
IntervalSource: IntervalSource,
LoopingSource: LoopingSource,
};
soundSets: { [key: string]: SoundSet } = SoundSets;


drag(ev: DragEvent): void {
ev.preventDefault();
dragSource(event: DragEvent): void {
if (event.dataTransfer) {
if (event.dataTransfer.types.includes("source")) event.preventDefault();
}
} }


drop(event: DragEvent): void {
dropSource(event: DragEvent): void {
event.preventDefault(); event.preventDefault();


if (event.dataTransfer) { if (event.dataTransfer) {
const label = event.dataTransfer.getData("text/plain");
const data = event.dataTransfer.getData("source");
const node = deserializeNode(JSON.parse(data));


const soundSet = this.soundSets[label];
this.soundscape.addSource(node as Source);
}
}


const source = new this.sources[soundSet.defaultSource](soundSet.name);
source.soundSet = soundSet;
this.soundscape.addSource(source);
// TODO
dragFilter(event: DragEvent): void {
if (event.dataTransfer) {
if (event.dataTransfer.types.includes("filter")) event.preventDefault();
}
}

dropFilter(event: DragEvent): void {
event.preventDefault();

if (event.dataTransfer) {
const data = event.dataTransfer.getData("filter");
const node = deserializeNode(JSON.parse(data));

this.soundscape.addFilter(node as Filter);
} }
} }


@@ -76,6 +90,8 @@ export default class SoundscapeComp extends Vue {


mounted(): void { mounted(): void {
this.soundscape.start(); this.soundscape.start();

console.log(this.soundscape);
} }
} }
</script> </script>


+ 17
- 4
src/components/nodes/FilterNode.vue Ver fichero

@@ -1,8 +1,15 @@
<template> <template>
<div :class="filter.active ? '' : 'inactive'" class="filter-node">
<Toggle class="active-toggle" v-model="filter.active" />
<div class="node-name">{{ filter.name }}</div>
<node-props :node="filter"></node-props>
<div>
<div
v-if="!dummy"
:class="filter.active ? '' : 'inactive'"
class="filter-node"
>
<Toggle class="active-toggle" v-model="filter.active" />
<div class="node-name">{{ filter.name }}</div>
<node-props :node="filter"></node-props>
</div>
<div v-if="dummy" class="filter-node dummy">Drop filters here!</div>
</div> </div>
</template> </template>


@@ -16,6 +23,7 @@ import Toggle from "@vueform/toggle";
@Options({ @Options({
props: { props: {
filter: Filter, filter: Filter,
dummy: Boolean,
}, },
components: { components: {
NodeProps, NodeProps,
@@ -24,6 +32,7 @@ import Toggle from "@vueform/toggle";
}) })
export default class FilterNode extends Vue { export default class FilterNode extends Vue {
filter!: Filter; filter!: Filter;
dummy = false;
} }
</script> </script>


@@ -59,4 +68,8 @@ export default class FilterNode extends Vue {
top: 10px; top: 10px;
left: 10px; left: 10px;
} }

.dummy {
min-height: 200px;
}
</style> </style>

+ 1
- 1
src/components/nodes/SourceNode.vue Ver fichero

@@ -10,7 +10,7 @@
<div class="node-name">{{ source.name }}</div> <div class="node-name">{{ source.name }}</div>
<node-props :node="source"></node-props> <node-props :node="source"></node-props>
</div> </div>
<div v-if="dummy" class="source-node dummy">Drop here!</div>
<div v-if="dummy" class="source-node dummy">Drop sounds here!</div>
</div> </div>
</template> </template>




+ 63
- 0
src/data/presets.ts Ver fichero

@@ -0,0 +1,63 @@
export const PresetSources: Array<{
name: string;
kind: "Source";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[x: string]: any;
}> = [
{
soundSet: {
name: "Gurgles",
soundKeys: [
"gurgles/gurgle (1)",
"gurgles/gurgle (2)",
"gurgles/gurgle (3)",
"gurgles/gurgle (4)",
"gurgles/gurgle (5)",
"gurgles/gurgle (6)",
"gurgles/gurgle (7)",
"gurgles/gurgle (8)",
"gurgles/gurgle (9)",
"gurgles/gurgle (10)",
"gurgles/gurgle (11)",
"gurgles/gurgle (12)",
"gurgles/gurgle (13)",
"gurgles/gurgle (14)",
"gurgles/gurgle (15)",
"gurgles/gurgle (16)",
"gurgles/gurgle (17)",
"gurgles/gurgle (18)",
"gurgles/gurgle (19)",
"gurgles/gurgle (20)",
"gurgles/gurgle (21)",
],
},
kind: "Source",
volume: 1,
interval: [4, 6],
pitch: [0.9, 1.1],
panning: [-0.2, 0.2],
name: "Gurgles",
type: "IntervalSource",
},
{
soundSet: {
name: "Squishing",
soundKeys: ["squishing"],
},
kind: "Source",
volume: 1,
pitch: 1,
name: "Squishing",
type: "LoopingSource",
},
];

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const PresetFilters: Array<{ name: string; [x: string]: any }> = [
{
cutoff: 1000,
name: "Lowpass Filter",
kind: "Filter",
type: "LowpassFilter",
},
];

+ 0
- 15
src/data/sound-sets.ts Ver fichero

@@ -1,15 +0,0 @@
import { SoundSet } from "@/sources/Source";

export const Gurgles: SoundSet = new SoundSet(
"Gurgles",
Array(21)
.fill(0)
.map((x, i) => "gurgles/gurgle (" + (i + 1) + ")"),
"IntervalSource"
);

export const Squishing: SoundSet = new SoundSet(
"Squishing",
["squishing"],
"LoopingSource"
);

+ 18
- 0
src/main.ts Ver fichero

@@ -1,4 +1,22 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { createApp } from "vue"; import { createApp } from "vue";
import Dissolve from "./Dissolve.vue"; import Dissolve from "./Dissolve.vue";
import { LowpassFilter } from "./filters/LowpassFilter";
import {
deserializeNode,
deserializeSoundscape,
serializeNode,
serializeSoundscape,
} from "./serialize";
import { IntervalSource } from "./sources/IntervalSource";
import { SoundSet } from "./sources/Source";


createApp(Dissolve).mount("#app"); createApp(Dissolve).mount("#app");

(window as any).IntervalSource = IntervalSource;
(window as any).LowpassFilter = LowpassFilter;
(window as any).SoundSet = SoundSet;
(window as any).serializeNode = serializeNode;
(window as any).deserializeNode = deserializeNode;
(window as any).serializeSoundscape = serializeSoundscape;
(window as any).deserializeSoundscape = deserializeSoundscape;

+ 150
- 0
src/serialize.ts Ver fichero

@@ -0,0 +1,150 @@
import {
exposedMetadataNumber,
exposedRangeMetadata,
exposedSoundSetMetadata,
NumberMetadata,
RangeMetadata,
Soundscape,
SoundSetMetadata,
} from "./audio";
import { IntervalSource } from "./sources/IntervalSource";
import { LoopingSource } from "./sources/LoopingSource";
import { Node } from "./audio";

const constructors: { [key: string]: new (name: string) => Node } = {
IntervalSource: IntervalSource,
LoopingSource: LoopingSource,
LowpassFilter: LowpassFilter,
HighpassFilter: HighpassFilter,
SterwoWidthFilter: StereoWidthFilter,
};

import { SoundSet, Source } from "./sources/Source";
import { Filter } from "./filters/Filter";
import { LowpassFilter } from "./filters/LowpassFilter";
import { HighpassFilter } from "./filters/HighpassFilter";
import { StereoWidthFilter } from "./filters/StereoWidthFilter";

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function serializeNode<T extends Node>(_node: T): any {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const results: any = {};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const node: any = _node;

Object.keys(node).forEach((key) => {
const numberMetadata: NumberMetadata | undefined = Reflect.getMetadata(
exposedMetadataNumber,
node,
key
);

if (numberMetadata !== undefined) {
results[key] = node[key];
}

const rangeMetadata: RangeMetadata | undefined = Reflect.getMetadata(
exposedRangeMetadata,
node,
key
);

if (rangeMetadata !== undefined) {
results[key] = node[key];
}

const soundSetMetadata: SoundSetMetadata | undefined = Reflect.getMetadata(
exposedSoundSetMetadata,
node,
key
);

if (soundSetMetadata !== undefined) {
const soundSet = node[key] as SoundSet;
results[key] = {
name: soundSet.name,
soundKeys: soundSet.soundKeys,
};
}
});

results.kind = node.kind;
results.name = node.name;
results.type = node.constructor.name;
return results;
}

// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
export function deserializeNode(data: any): Node {
const constructor = constructors[data.type];

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const node: any = new constructor(data.name);

Object.keys(node).forEach((key) => {
const numberMetadata: NumberMetadata | undefined = Reflect.getMetadata(
exposedMetadataNumber,
node,
key
);

if (numberMetadata !== undefined) {
node[key] = data[key];
}

const rangeMetadata: RangeMetadata | undefined = Reflect.getMetadata(
exposedRangeMetadata,
node,
key
);

if (rangeMetadata !== undefined) {
node[key] = data[key];
}

const soundSetMetadata: SoundSetMetadata | undefined = Reflect.getMetadata(
exposedSoundSetMetadata,
node,
key
);

if (soundSetMetadata !== undefined) {
node[key] = new SoundSet(data[key].name, data[key].soundKeys);
}
});

return node as Node;
}

export type SerializedSoundscape = {
name: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
sources: Array<any>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
filters: Array<any>;
};

export function serializeSoundscape(scape: Soundscape): SerializedSoundscape {
const result = {
name: scape.name,
sources: scape.sources.map((source) => serializeNode(source)),
filters: scape.filters.map((filter) => serializeNode(filter)),
};

return result;
}

export function deserializeSoundscape(data: SerializedSoundscape): Soundscape {
const scape = new Soundscape();
scape.name = data.name;

data.sources
.map((source) => deserializeNode(source) as Source)
.forEach((source) => scape.addSource(source));

data.filters
.map((filter) => deserializeNode(filter) as Filter)
.forEach((filter) => scape.addFilter(filter));

return scape;
}

+ 0
- 2
src/sources/IntervalSource.ts Ver fichero

@@ -2,8 +2,6 @@ import { Source } from "./Source";
import { context, exposedRange } from "../audio"; import { context, exposedRange } from "../audio";


export class IntervalSource extends Source { export class IntervalSource extends Source {
kind = "Interval";

@exposedRange({ @exposedRange({
name: "Interval", name: "Interval",
min: 0.25, min: 0.25,


+ 14
- 1
src/sources/LoopingSource.ts Ver fichero

@@ -1,8 +1,13 @@
import { Source } from "./Source"; import { Source } from "./Source";
import { context, exposedNumber } from "../audio"; import { context, exposedNumber } from "../audio";


export type SerializedLoopingSource = {
name: string;
volume: number;
pitch: number;
};

export class LoopingSource extends Source { export class LoopingSource extends Source {
kind = "Looping";
private source!: AudioBufferSourceNode; private source!: AudioBufferSourceNode;
private started = false; private started = false;
private running = false; private running = false;
@@ -21,6 +26,14 @@ export class LoopingSource extends Source {
super(name); super(name);
} }


static deserialize(info: SerializedLoopingSource): LoopingSource {
const source = new LoopingSource(info.name);
source.volume = info.volume;
source.pitch = info.pitch;

return source;
}

public start(): void { public start(): void {
this.started = true; this.started = true;
} }


+ 14
- 8
src/sources/Source.ts Ver fichero

@@ -1,14 +1,16 @@
import { Node, context, exposedNumber, loadAudio } from "../audio";
import {
Node,
context,
exposedNumber,
loadAudio,
exposedSoundSet,
} from "../audio";


export class SoundSet { export class SoundSet {
soundMap: Map<string, AudioBuffer> = new Map(); soundMap: Map<string, AudioBuffer> = new Map();
soundList: Array<AudioBuffer> = []; soundList: Array<AudioBuffer> = [];


constructor(
public name: string,
public soundKeys: Array<string>,
public defaultSource: string
) {
constructor(public name: string, public soundKeys: Array<string>) {
this.soundKeys.forEach((sound) => { this.soundKeys.forEach((sound) => {
loadAudio(sound, this); loadAudio(sound, this);
}); });
@@ -21,8 +23,12 @@ export class SoundSet {
} }


export abstract class Source extends Node { export abstract class Source extends Node {
public abstract kind: string;
public soundSet: SoundSet = new SoundSet("Empty", [], "IntervalSource");
kind = "Source";

@exposedSoundSet({
name: "Sounds",
})
public soundSet: SoundSet = new SoundSet("Empty", []);
public gain: GainNode; public gain: GainNode;
public output: GainNode; public output: GainNode;
public _active = true; public _active = true;


Cargando…
Cancelar
Guardar