| @@ -1,6 +1,6 @@ | |||
| import "reflect-metadata"; | |||
| import { Filter } from "./filters/Filter"; | |||
| import { Source } from "./sources/Source"; | |||
| import { SoundSet, Source } from "./sources/Source"; | |||
| let ogg_support = false; | |||
| @@ -18,6 +18,19 @@ export class Soundscape { | |||
| 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 { | |||
| if (this.filters.length > 0) { | |||
| const last: Filter = this.filters[this.filters.length - 1]; | |||
| @@ -90,7 +103,7 @@ export let context: AudioContext; | |||
| 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(); | |||
| // decide if we can load oggs | |||
| @@ -100,28 +113,31 @@ export function audioTest(): void { | |||
| } | |||
| // 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 | |||
| 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? | |||
| if (audioDict.has(name) && !flush) { | |||
| 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? | |||
| if (!flush) { | |||
| @@ -139,14 +155,14 @@ function cacheAndParse(name: string, data: ArrayBuffer) { | |||
| function parseAudioData(name: string, data: ArrayBuffer) { | |||
| context.decodeAudioData( | |||
| 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> | |||
| <div id="menu"> | |||
| <div class="list-label">Sources</div> | |||
| <div class="list-label">Sounds</div> | |||
| <div class="list"> | |||
| <draggable | |||
| v-for="(source, index) in sourceTypes" | |||
| v-for="(source, index) in soundSets" | |||
| :key="index" | |||
| :label="source" | |||
| :label="source.name" | |||
| /> | |||
| </div> | |||
| </div> | |||
| @@ -14,6 +14,8 @@ | |||
| <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"; | |||
| @Options({ | |||
| components: { | |||
| @@ -21,17 +23,7 @@ import Draggable from "@/components/Draggable.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> | |||
| @@ -4,6 +4,7 @@ | |||
| v-for="(source, index) in soundscape.sources" | |||
| :key="index" | |||
| :source="source" | |||
| v-on:delete="deleteSource(source)" | |||
| > | |||
| </source-node> | |||
| <source-node | |||
| @@ -26,8 +27,10 @@ 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 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({ | |||
| props: { | |||
| @@ -42,16 +45,11 @@ export default class SoundscapeComp extends Vue { | |||
| soundscape!: Soundscape; | |||
| started = false; | |||
| 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 { | |||
| ev.preventDefault(); | |||
| @@ -63,11 +61,19 @@ export default class SoundscapeComp extends Vue { | |||
| if (event.dataTransfer) { | |||
| 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 { | |||
| this.soundscape.start(); | |||
| } | |||
| @@ -5,7 +5,8 @@ | |||
| :class="source.active ? '' : 'inactive'" | |||
| 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> | |||
| <node-props :node="source"></node-props> | |||
| </div> | |||
| @@ -28,9 +29,11 @@ import Toggle from "@vueform/toggle"; | |||
| NodeProps, | |||
| Toggle, | |||
| }, | |||
| emits: ["delete"], | |||
| }) | |||
| export default class SourceNode extends Vue { | |||
| source!: Source; | |||
| dummy = false; | |||
| } | |||
| </script> | |||
| @@ -69,4 +72,13 @@ export default class SourceNode extends Vue { | |||
| .dummy { | |||
| min-height: 200px; | |||
| } | |||
| .delete-button { | |||
| position: absolute; | |||
| top: 5px; | |||
| right: 5px; | |||
| width: 25px; | |||
| height: 25px; | |||
| font-size: 24px; | |||
| } | |||
| </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) { | |||
| this.remaining -= dt; | |||
| 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(); | |||
| node.buffer = this.sounds[index]; | |||
| node.buffer = this.soundSet.soundList[index]; | |||
| const pan = context.createStereoPanner(); | |||
| pan.pan.value = | |||
| @@ -26,9 +26,9 @@ export class LoopingSource extends Source { | |||
| } | |||
| 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.buffer = this.sounds[index]; | |||
| this.source.buffer = this.soundSet.soundList[index]; | |||
| this.source.connect(this.gain); | |||
| this.source.onended = () => { | |||
| this.pickRandom(); | |||
| @@ -37,7 +37,7 @@ export class LoopingSource extends Source { | |||
| } | |||
| public tick(dt: number): void { | |||
| 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.source.start(); | |||
| 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"; | |||
| 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 { | |||
| public abstract kind: string; | |||
| public sounds: Array<AudioBuffer> = []; | |||
| public soundSet: SoundSet = new SoundSet("Empty", [], "IntervalSource"); | |||
| public gain: GainNode; | |||
| public output: GainNode; | |||
| public _active = true; | |||
| @@ -35,16 +55,9 @@ export abstract class Source extends Node { | |||
| 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; | |||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | |||
| public tick(dt: number): void { | |||
| this.gain.gain.value = this.volume; | |||
| } | |||