| @@ -1,6 +1,6 @@ | |||||
| import "reflect-metadata"; | import "reflect-metadata"; | ||||
| import { Filter } from "./filters/Filter"; | import { Filter } from "./filters/Filter"; | ||||
| import { Source } from "./sources/Source"; | |||||
| import { SoundSet, Source } from "./sources/Source"; | |||||
| let ogg_support = false; | let ogg_support = false; | ||||
| @@ -18,6 +18,19 @@ export class Soundscape { | |||||
| source.start(); | source.start(); | ||||
| } | } | ||||
| removeSource(source: Source): void { | |||||
| if (this.sources.includes(source)) { | |||||
| source.output.disconnect(); | |||||
| this.sources = this.sources.filter((x) => x !== source); | |||||
| } else { | |||||
| console.warn( | |||||
| "Tried to remove a source from a Soundscape that wasn't using it" | |||||
| ); | |||||
| console.warn(this); | |||||
| console.warn(source); | |||||
| } | |||||
| } | |||||
| addFilter(filter: Filter): void { | addFilter(filter: Filter): void { | ||||
| if (this.filters.length > 0) { | if (this.filters.length > 0) { | ||||
| const last: Filter = this.filters[this.filters.length - 1]; | const last: Filter = this.filters[this.filters.length - 1]; | ||||
| @@ -90,7 +103,7 @@ export let context: AudioContext; | |||||
| const audioBaseUrl = "/audio/"; | const audioBaseUrl = "/audio/"; | ||||
| const waiting: Map<string, Array<Source>> = new Map(); | |||||
| const waiting: Map<string, Array<SoundSet>> = new Map(); | |||||
| const audioDict: Map<string, AudioBuffer> = new Map(); | const audioDict: Map<string, AudioBuffer> = new Map(); | ||||
| // decide if we can load oggs | // decide if we can load oggs | ||||
| @@ -100,28 +113,31 @@ export function audioTest(): void { | |||||
| } | } | ||||
| // asynchronously load an audio file | // asynchronously load an audio file | ||||
| export function loadAudio(name: string, source: Source, flush = false): void { | |||||
| export function loadAudio(name: string, client: SoundSet, flush = false): void { | |||||
| // pick a format | // pick a format | ||||
| name += ogg_support ? ".ogg" : ".mp3"; | name += ogg_support ? ".ogg" : ".mp3"; | ||||
| // are we already trying to get the audio? | |||||
| if (!waiting.has(name)) { | |||||
| waiting.set(name, []); | |||||
| } | |||||
| const list: Array<Source> | undefined = waiting.get(name); | |||||
| if (list !== undefined) list.push(source); | |||||
| // do we already have the audio? | // do we already have the audio? | ||||
| if (audioDict.has(name) && !flush) { | if (audioDict.has(name) && !flush) { | ||||
| const buf: AudioBuffer | undefined = audioDict.get(name); | const buf: AudioBuffer | undefined = audioDict.get(name); | ||||
| if (buf !== undefined) source.addLoadedSound(buf); | |||||
| if (buf !== undefined) client.loadSound(name, buf); | |||||
| return; | |||||
| } | |||||
| // are we already trying to get the audio? | |||||
| if (!waiting.has(name)) { | |||||
| waiting.set(name, []); | |||||
| } | } | ||||
| const list: Array<SoundSet> | undefined = waiting.get(name); | |||||
| if (list !== undefined) list.push(client); | |||||
| // is the audio already stored locally? | // is the audio already stored locally? | ||||
| if (!flush) { | if (!flush) { | ||||
| @@ -139,14 +155,14 @@ function cacheAndParse(name: string, data: ArrayBuffer) { | |||||
| function parseAudioData(name: string, data: ArrayBuffer) { | function parseAudioData(name: string, data: ArrayBuffer) { | ||||
| context.decodeAudioData( | context.decodeAudioData( | ||||
| data, | data, | ||||
| function (buffer) { | |||||
| audioDict.set(name, buffer); | |||||
| function (buf) { | |||||
| audioDict.set(name, buf); | |||||
| const waitingSources: Array<Source> | undefined = waiting.get(name); | |||||
| const waitingClients: Array<SoundSet> | undefined = waiting.get(name); | |||||
| if (waitingSources !== undefined) { | |||||
| waitingSources.forEach((source) => { | |||||
| source.addLoadedSound(buffer); | |||||
| if (waitingClients !== undefined) { | |||||
| waitingClients.forEach((client) => { | |||||
| client.loadSound(name, buf); | |||||
| }); | }); | ||||
| } | } | ||||
| }, | }, | ||||
| @@ -1,11 +1,11 @@ | |||||
| <template> | <template> | ||||
| <div id="menu"> | <div id="menu"> | ||||
| <div class="list-label">Sources</div> | |||||
| <div class="list-label">Sounds</div> | |||||
| <div class="list"> | <div class="list"> | ||||
| <draggable | <draggable | ||||
| v-for="(source, index) in sourceTypes" | |||||
| v-for="(source, index) in soundSets" | |||||
| :key="index" | :key="index" | ||||
| :label="source" | |||||
| :label="source.name" | |||||
| /> | /> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| @@ -14,6 +14,8 @@ | |||||
| <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"; | |||||
| @Options({ | @Options({ | ||||
| components: { | components: { | ||||
| @@ -21,17 +23,7 @@ import Draggable from "@/components/Draggable.vue"; | |||||
| }, | }, | ||||
| }) | }) | ||||
| export default class Menu extends Vue { | export default class Menu extends Vue { | ||||
| sourceTypes = [ | |||||
| "Rumble", | |||||
| "Glorps", | |||||
| "Heartbeat", | |||||
| "Breathing", | |||||
| "Squishing", | |||||
| "Burps", | |||||
| "Gurgles", | |||||
| ]; | |||||
| foo = 3; | |||||
| soundSets: Array<SoundSet> = Array.from(Object.values(SoundSets)); | |||||
| } | } | ||||
| </script> | </script> | ||||
| @@ -4,6 +4,7 @@ | |||||
| v-for="(source, index) in soundscape.sources" | v-for="(source, index) in soundscape.sources" | ||||
| :key="index" | :key="index" | ||||
| :source="source" | :source="source" | ||||
| v-on:delete="deleteSource(source)" | |||||
| > | > | ||||
| </source-node> | </source-node> | ||||
| <source-node | <source-node | ||||
| @@ -26,8 +27,10 @@ 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 Sources from "@/sources/PremadeSources"; | |||||
| import { Source } from "@/sources/Source"; | |||||
| import * as SoundSets from "@/data/sound-sets"; | |||||
| import { SoundSet, Source } from "@/sources/Source"; | |||||
| import { IntervalSource } from "@/sources/IntervalSource"; | |||||
| import { LoopingSource } from "@/sources/LoopingSource"; | |||||
| @Options({ | @Options({ | ||||
| props: { | props: { | ||||
| @@ -42,16 +45,11 @@ export default class SoundscapeComp extends Vue { | |||||
| soundscape!: Soundscape; | soundscape!: Soundscape; | ||||
| started = false; | started = false; | ||||
| context!: AudioContext; | context!: AudioContext; | ||||
| makers: Record<string, () => Source> = { | |||||
| Gurgles: Sources.makeGurgles, | |||||
| Burps: Sources.makeBurps, | |||||
| Glorps: Sources.makeGlorps, | |||||
| Squishing: Sources.makeSquishing, | |||||
| Heartbeat: Sources.makeHeartbeat, | |||||
| Breathing: Sources.makeBreathing, | |||||
| Rumble: Sources.makeRumble, | |||||
| sources: { [key: string]: new (name: string) => Source } = { | |||||
| IntervalSource: IntervalSource, | |||||
| LoopingSource: LoopingSource, | |||||
| }; | }; | ||||
| soundSets: { [key: string]: SoundSet } = SoundSets; | |||||
| drag(ev: DragEvent): void { | drag(ev: DragEvent): void { | ||||
| ev.preventDefault(); | ev.preventDefault(); | ||||
| @@ -63,11 +61,19 @@ export default class SoundscapeComp extends Vue { | |||||
| if (event.dataTransfer) { | if (event.dataTransfer) { | ||||
| const label = event.dataTransfer.getData("text/plain"); | const label = event.dataTransfer.getData("text/plain"); | ||||
| console.log(this.makers[label]) | |||||
| this.soundscape.addSource(this.makers[label]()); | |||||
| const soundSet = this.soundSets[label]; | |||||
| const source = new this.sources[soundSet.defaultSource](soundSet.name); | |||||
| source.soundSet = soundSet; | |||||
| this.soundscape.addSource(source); | |||||
| // TODO | |||||
| } | } | ||||
| } | } | ||||
| deleteSource(source: Source): void { | |||||
| this.soundscape.removeSource(source); | |||||
| } | |||||
| mounted(): void { | mounted(): void { | ||||
| this.soundscape.start(); | this.soundscape.start(); | ||||
| } | } | ||||
| @@ -5,7 +5,8 @@ | |||||
| :class="source.active ? '' : 'inactive'" | :class="source.active ? '' : 'inactive'" | ||||
| class="source-node" | class="source-node" | ||||
| > | > | ||||
| <Toggle class="active-toggle" v-model="source.active" /> | |||||
| <button class="delete-button" v-on:click="$emit('delete')">X</button> | |||||
| <toggle class="active-toggle" v-model="source.active" /> | |||||
| <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> | ||||
| @@ -28,9 +29,11 @@ import Toggle from "@vueform/toggle"; | |||||
| NodeProps, | NodeProps, | ||||
| Toggle, | Toggle, | ||||
| }, | }, | ||||
| emits: ["delete"], | |||||
| }) | }) | ||||
| export default class SourceNode extends Vue { | export default class SourceNode extends Vue { | ||||
| source!: Source; | source!: Source; | ||||
| dummy = false; | |||||
| } | } | ||||
| </script> | </script> | ||||
| @@ -69,4 +72,13 @@ export default class SourceNode extends Vue { | |||||
| .dummy { | .dummy { | ||||
| min-height: 200px; | min-height: 200px; | ||||
| } | } | ||||
| .delete-button { | |||||
| position: absolute; | |||||
| top: 5px; | |||||
| right: 5px; | |||||
| width: 25px; | |||||
| height: 25px; | |||||
| font-size: 24px; | |||||
| } | |||||
| </style> | </style> | ||||
| @@ -0,0 +1,15 @@ | |||||
| 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" | |||||
| ); | |||||
| @@ -71,11 +71,13 @@ export class IntervalSource extends Source { | |||||
| if (this.started) { | if (this.started) { | ||||
| this.remaining -= dt; | this.remaining -= dt; | ||||
| if (this.remaining <= 0) { | if (this.remaining <= 0) { | ||||
| const index = Math.floor(Math.random() * this.sounds.length); | |||||
| const index = Math.floor( | |||||
| Math.random() * this.soundSet.soundList.length | |||||
| ); | |||||
| const node = context.createBufferSource(); | const node = context.createBufferSource(); | ||||
| node.buffer = this.sounds[index]; | |||||
| node.buffer = this.soundSet.soundList[index]; | |||||
| const pan = context.createStereoPanner(); | const pan = context.createStereoPanner(); | ||||
| pan.pan.value = | pan.pan.value = | ||||
| @@ -26,9 +26,9 @@ export class LoopingSource extends Source { | |||||
| } | } | ||||
| private pickRandom(): void { | private pickRandom(): void { | ||||
| const index = Math.floor(Math.random() * this.sounds.length); | |||||
| const index = Math.floor(Math.random() * this.soundSet.soundList.length); | |||||
| this.source = context.createBufferSource(); | this.source = context.createBufferSource(); | ||||
| this.source.buffer = this.sounds[index]; | |||||
| this.source.buffer = this.soundSet.soundList[index]; | |||||
| this.source.connect(this.gain); | this.source.connect(this.gain); | ||||
| this.source.onended = () => { | this.source.onended = () => { | ||||
| this.pickRandom(); | this.pickRandom(); | ||||
| @@ -37,7 +37,7 @@ export class LoopingSource extends Source { | |||||
| } | } | ||||
| public tick(dt: number): void { | public tick(dt: number): void { | ||||
| super.tick(dt); | super.tick(dt); | ||||
| if (this.started && this.sounds.length > 0 && !this.running) { | |||||
| if (this.started && this.soundSet.soundList.length > 0 && !this.running) { | |||||
| this.pickRandom(); | this.pickRandom(); | ||||
| this.source.start(); | this.source.start(); | ||||
| this.running = true; | this.running = true; | ||||
| @@ -1,114 +0,0 @@ | |||||
| import { IntervalSource } from "./IntervalSource"; | |||||
| import { LoopingSource } from "./LoopingSource"; | |||||
| import { Source } from "./Source"; | |||||
| export function makeGlorps(): Source { | |||||
| const source: IntervalSource = new IntervalSource("Guts"); | |||||
| source.loadSound("bowels-to-intestines"); | |||||
| source.loadSound("intestines-to-bowels"); | |||||
| source.loadSound("intestines-to-stomach"); | |||||
| source.loadSound("intestines-to-stomach-forced"); | |||||
| source.loadSound("stomach-to-intestines"); | |||||
| source.loadSound("stomach-to-intestines-fail"); | |||||
| source.loadSound("stomach-churn"); | |||||
| source.loadSound("bowels-churn-safe"); | |||||
| source.loadSound("bowels-churn-danger"); | |||||
| console.log(source); | |||||
| source.interval = [4, 8]; | |||||
| source.pitch = [0.75, 1.25]; | |||||
| return source; | |||||
| } | |||||
| export function makeBurps(): Source { | |||||
| const source: IntervalSource = new IntervalSource("Burps"); | |||||
| source.loadSound("belch (1)"); | |||||
| source.loadSound("belch (2)"); | |||||
| source.loadSound("belch (3)"); | |||||
| source.loadSound("belch (4)"); | |||||
| source.loadSound("belch (5)"); | |||||
| source.loadSound("belch (6)"); | |||||
| source.loadSound("belch (7)"); | |||||
| source.loadSound("belch (8)"); | |||||
| source.loadSound("belch (9)"); | |||||
| source.loadSound("belch (10)"); | |||||
| source.loadSound("belch (11)"); | |||||
| source.loadSound("belch (12)"); | |||||
| source.loadSound("belch (13)"); | |||||
| source.loadSound("belch (14)"); | |||||
| source.loadSound("belch (15)"); | |||||
| source.loadSound("belch (16)"); | |||||
| source.interval = [10, 30]; | |||||
| source.pitch = [0.8, 1.1]; | |||||
| source.active = false; | |||||
| return source; | |||||
| } | |||||
| export function makeGurgles(): Source { | |||||
| const source: IntervalSource = new IntervalSource("Gurgles"); | |||||
| source.loadSound("gurgles/gurgle (1)"); | |||||
| source.loadSound("gurgles/gurgle (2)"); | |||||
| source.loadSound("gurgles/gurgle (3)"); | |||||
| source.loadSound("gurgles/gurgle (4)"); | |||||
| source.loadSound("gurgles/gurgle (5)"); | |||||
| source.loadSound("gurgles/gurgle (6)"); | |||||
| source.loadSound("gurgles/gurgle (7)"); | |||||
| source.loadSound("gurgles/gurgle (8)"); | |||||
| source.loadSound("gurgles/gurgle (9)"); | |||||
| source.loadSound("gurgles/gurgle (10)"); | |||||
| source.loadSound("gurgles/gurgle (11)"); | |||||
| source.loadSound("gurgles/gurgle (12)"); | |||||
| source.loadSound("gurgles/gurgle (13)"); | |||||
| source.loadSound("gurgles/gurgle (14)"); | |||||
| source.loadSound("gurgles/gurgle (15)"); | |||||
| source.loadSound("gurgles/gurgle (16)"); | |||||
| source.loadSound("gurgles/gurgle (17)"); | |||||
| source.loadSound("gurgles/gurgle (18)"); | |||||
| source.loadSound("gurgles/gurgle (19)"); | |||||
| source.loadSound("gurgles/gurgle (20)"); | |||||
| source.loadSound("gurgles/gurgle (21)"); | |||||
| source.pitch = [0.6, 1.2]; | |||||
| source.interval = [2, 10]; | |||||
| source.panning = [-0.6, 0.6]; | |||||
| return source; | |||||
| } | |||||
| export function makeHeartbeat(): LoopingSource { | |||||
| const source: LoopingSource = new LoopingSource("Heartbeat"); | |||||
| source.loadSound("heartbeat"); | |||||
| source.volume = 0.3; | |||||
| return source; | |||||
| } | |||||
| export function makeBreathing(): LoopingSource { | |||||
| const source: LoopingSource = new LoopingSource("Breathing"); | |||||
| source.loadSound("breaths"); | |||||
| return source; | |||||
| } | |||||
| export function makeRumble(): LoopingSource { | |||||
| const source: LoopingSource = new LoopingSource("Rumble"); | |||||
| source.loadSound("rumble"); | |||||
| return source; | |||||
| } | |||||
| export function makeSquishing(): LoopingSource { | |||||
| const source: LoopingSource = new LoopingSource("Squishing"); | |||||
| source.loadSound("squishing"); | |||||
| return source; | |||||
| } | |||||
| @@ -1,8 +1,28 @@ | |||||
| import { Node, context, exposedNumber, loadAudio } from "../audio"; | import { Node, context, exposedNumber, loadAudio } 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 | |||||
| ) { | |||||
| this.soundKeys.forEach((sound) => { | |||||
| loadAudio(sound, this); | |||||
| }); | |||||
| } | |||||
| public loadSound(name: string, buf: AudioBuffer): void { | |||||
| this.soundList.push(buf); | |||||
| this.soundMap.set(name, buf); | |||||
| } | |||||
| } | |||||
| export abstract class Source extends Node { | export abstract class Source extends Node { | ||||
| public abstract kind: string; | public abstract kind: string; | ||||
| public sounds: Array<AudioBuffer> = []; | |||||
| public soundSet: SoundSet = new SoundSet("Empty", [], "IntervalSource"); | |||||
| public gain: GainNode; | public gain: GainNode; | ||||
| public output: GainNode; | public output: GainNode; | ||||
| public _active = true; | public _active = true; | ||||
| @@ -35,16 +55,9 @@ export abstract class Source extends Node { | |||||
| this.gain.connect(this.output); | this.gain.connect(this.output); | ||||
| } | } | ||||
| public loadSound(name: string): void { | |||||
| loadAudio(name, this); | |||||
| } | |||||
| public addLoadedSound(sound: AudioBuffer): void { | |||||
| this.sounds.push(sound); | |||||
| } | |||||
| public abstract start(): void; | public abstract start(): void; | ||||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | |||||
| public tick(dt: number): void { | public tick(dt: number): void { | ||||
| this.gain.gain.value = this.volume; | this.gain.gain.value = this.volume; | ||||
| } | } | ||||