瀏覽代碼

Implement serialization and deserialization for nodes and soundscapes

master
Fen Dweller 4 年之前
父節點
當前提交
c9cd0f99b9
共有 13 個文件被更改,包括 346 次插入63 次删除
  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 查看文件

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

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

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

export type SoundSetMetadata = PropMetadata;

export const exposedMetadataNumber = Symbol("exposedNumber");

// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
@@ -99,6 +103,13 @@ export function exposedRange(options: RangeMetadata) {
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;

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

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

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

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

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

console.log(req);

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


+ 4
- 5
src/components/Draggable.vue 查看文件

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

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

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

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>


+ 13
- 5
src/components/Menu.vue 查看文件

@@ -3,9 +3,17 @@
<div class="list-label">Sounds</div>
<div class="list">
<draggable
v-for="(source, index) in soundSets"
v-for="(source, index) in presetSources"
: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>
@@ -14,8 +22,7 @@
<script lang="ts">
import { Options, Vue } from "vue-class-component";
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({
components: {
@@ -23,7 +30,8 @@ import { SoundSet } from "@/sources/Source";
},
})
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>



+ 36
- 20
src/components/SoundscapeComp.vue 查看文件

@@ -8,8 +8,8 @@
>
</source-node>
<source-node
v-on:drop="drop"
v-on:dragover="drag"
v-on:drop="dropSource"
v-on:dragover="dragSource"
:dummy="true"
></source-node>
<filter-node
@@ -18,6 +18,11 @@
:filter="filter"
>
</filter-node>
<filter-node
v-on:drop="dropFilter"
v-on:dragover="dragFilter"
:dummy="true"
></filter-node>
</div>
<div></div>
</template>
@@ -27,10 +32,9 @@ import { Soundscape } from "@/audio";
import { Options, Vue } from "vue-class-component";
import SourceNode from "./nodes/SourceNode.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({
props: {
@@ -45,28 +49,38 @@ export default class SoundscapeComp extends Vue {
soundscape!: Soundscape;
started = false;
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();

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 {
this.soundscape.start();

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


+ 17
- 4
src/components/nodes/FilterNode.vue 查看文件

@@ -1,8 +1,15 @@
<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>
</template>

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

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

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

+ 1
- 1
src/components/nodes/SourceNode.vue 查看文件

@@ -10,7 +10,7 @@
<div class="node-name">{{ source.name }}</div>
<node-props :node="source"></node-props>
</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>
</template>



+ 63
- 0
src/data/presets.ts 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -1,4 +1,22 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { createApp } from "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");

(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 查看文件

@@ -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 查看文件

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

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

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


+ 14
- 1
src/sources/LoopingSource.ts 查看文件

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

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

export class LoopingSource extends Source {
kind = "Looping";
private source!: AudioBufferSourceNode;
private started = false;
private running = false;
@@ -21,6 +26,14 @@ export class LoopingSource extends Source {
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 {
this.started = true;
}


+ 14
- 8
src/sources/Source.ts 查看文件

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

export class SoundSet {
soundMap: Map<string, AudioBuffer> = new Map();
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) => {
loadAudio(sound, this);
});
@@ -21,8 +23,12 @@ export class SoundSet {
}

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 output: GainNode;
public _active = true;


Loading…
取消
儲存