| oid sha256:738ab74eb62ed614c5277db740ef0655a6cc9401f5bd455fd44456134dc1135f | |||||
| size 588848 |
| oid sha256:308aaf542df05a3fadee0502ba1fc4c4032ff05d7b784cb76dfd2689bbd13311 | |||||
| size 131212 |
| oid sha256:c6ae415683698a57ba1f391eabbc9081a3c55646249ef54414e89790229b26a8 | |||||
| size 242240 |
| oid sha256:41bdc4f3f67021838f9b48e6688c5d7a8e9346f3f838c92dd42129b16328aaad | |||||
| size 98522 |
| oid sha256:fa2992b43a6e0a8f9d96bfc0969b5264791cd6915e2ac8f4f2fb29e4112f5ae8 | |||||
| size 722480 |
| oid sha256:d4f743617fcd80131b01331223059c5c255211663aed233534e0bd6309c8be00 | |||||
| size 398274 |
| oid sha256:e1a56cfd4c4a34817aa53ffc521d88465e435b4fd85ce17ddf75ba0ecb4e3383 | |||||
| size 318452 |
| oid sha256:abbcc45a94772dddd22c5545789e0ae2bb16b99d9394c27bb8ddef4c45ada52c | |||||
| size 435013 |
| @@ -7,8 +7,11 @@ | |||||
| <div> | <div> | ||||
| Many sounds by <a href="https://www.furaffinity.net/user/jeschke">Jit</a>! | Many sounds by <a href="https://www.furaffinity.net/user/jeschke">Jit</a>! | ||||
| </div> | </div> | ||||
| <button v-on:click="start" class="start-button"> | |||||
| {{ started ? "Add Soundscape" : "Start" }} | |||||
| <button v-on:click="start" class="start-button" v-if="!started"> | |||||
| Start | |||||
| </button> | |||||
| <button v-on:click="addSoundscape" class="start-button" v-if="started"> | |||||
| Add Soundscape | |||||
| </button> | </button> | ||||
| <SoundscapeComp | <SoundscapeComp | ||||
| v-for="(soundscape, index) in soundscapes" | v-for="(soundscape, index) in soundscapes" | ||||
| @@ -23,25 +26,75 @@ | |||||
| import { Options, Vue } from "vue-class-component"; | import { Options, Vue } from "vue-class-component"; | ||||
| import { clearCache, setup, Soundscape } from "./audio"; | import { clearCache, setup, Soundscape } from "./audio"; | ||||
| import SoundscapeComp from "./components/SoundscapeComp.vue"; | import SoundscapeComp from "./components/SoundscapeComp.vue"; | ||||
| import { Filter } from "./filters/Filter"; | |||||
| import { HighpassFilter } from "./filters/HighpassFilter"; | |||||
| import { LowpassFilter } from "./filters/LowpassFilter"; | |||||
| import { StereoWidthFilter } from "./filters/StereoWidthFilter"; | |||||
| import { LoopingSource } from "./sources/LoopingSource"; | |||||
| import * as Sources from "./sources/PremadeSources"; | |||||
| @Options({ | @Options({ | ||||
| components: { | components: { | ||||
| SoundscapeComp, | SoundscapeComp, | ||||
| }, | }, | ||||
| }) | }) | ||||
| export default class Dissolve extends Vue { | export default class Dissolve extends Vue { | ||||
| context!: AudioContext; | |||||
| started = false; | started = false; | ||||
| soundscapes: Array<Soundscape> = []; | soundscapes: Array<Soundscape> = []; | ||||
| addSoundscape(): void { | |||||
| const scape: Soundscape = new Soundscape(); | |||||
| scape.addSource(Sources.makeGlorps()); | |||||
| scape.addSource(Sources.makeGurgles()); | |||||
| scape.addSource(Sources.makeHeartbeat()); | |||||
| scape.addSource(Sources.makeRumble()); | |||||
| scape.addSource(Sources.makeSquishing()); | |||||
| scape.addSource(Sources.makeBreathing()); | |||||
| scape.addSource(Sources.makeBurps()); | |||||
| scape.addFilter(new LowpassFilter()); | |||||
| scape.addFilter(new HighpassFilter()); | |||||
| scape.addFilter(new StereoWidthFilter()); | |||||
| scape.output.connect(this.context.destination); | |||||
| this.soundscapes.push(scape); | |||||
| } | |||||
| start(): void { | start(): void { | ||||
| this.started = true; | this.started = true; | ||||
| const scape: Soundscape = new Soundscape(); | |||||
| this.soundscapes.push(scape); | |||||
| const internal: Soundscape = new Soundscape(); | |||||
| internal.addSource(Sources.makeGlorps()); | |||||
| internal.addSource(Sources.makeGurgles()); | |||||
| internal.addSource(Sources.makeHeartbeat()); | |||||
| internal.addSource(Sources.makeRumble()); | |||||
| internal.addSource(Sources.makeSquishing()); | |||||
| internal.output.connect(this.context.destination); | |||||
| this.soundscapes.push(internal); | |||||
| const external: Soundscape = new Soundscape(); | |||||
| const breathing: LoopingSource = Sources.makeBreathing(); | |||||
| breathing.volume = 0.3; | |||||
| external.addSource(breathing); | |||||
| external.addSource(Sources.makeBurps()); | |||||
| const lowpass: Filter = new LowpassFilter(); | |||||
| lowpass.active = true; | |||||
| external.addFilter(lowpass); | |||||
| const highpass: Filter = new HighpassFilter(); | |||||
| highpass.active = false; | |||||
| external.addFilter(highpass); | |||||
| external.output.connect(this.context.destination); | |||||
| this.soundscapes.push(external); | |||||
| } | } | ||||
| mounted(): void { | mounted(): void { | ||||
| setup(); | |||||
| this.context = setup(); | |||||
| } | } | ||||
| clear(): void { | clear(): void { | ||||
| @@ -5,10 +5,12 @@ import { Source } from "./sources/Source"; | |||||
| let ogg_support = false; | let ogg_support = false; | ||||
| export class Soundscape { | export class Soundscape { | ||||
| public name = "Soundscape"; | |||||
| public sources: Array<Source> = []; | public sources: Array<Source> = []; | ||||
| public filters: Array<Filter> = []; | public filters: Array<Filter> = []; | ||||
| public filterBus: GainNode; | public filterBus: GainNode; | ||||
| public output: GainNode; | |||||
| addSource(source: Source): void { | addSource(source: Source): void { | ||||
| source.output.connect(this.filterBus); | source.output.connect(this.filterBus); | ||||
| @@ -42,6 +44,8 @@ export class Soundscape { | |||||
| constructor() { | constructor() { | ||||
| this.filterBus = context.createGain(); | this.filterBus = context.createGain(); | ||||
| this.output = context.createGain(); | |||||
| this.filterBus.connect(this.output); | |||||
| } | } | ||||
| } | } | ||||
| export abstract class Node { | export abstract class Node { | ||||
| @@ -21,11 +21,6 @@ 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 { Filter } from "@/filters/Filter"; | |||||
| import { BiquadFilter } from "@/filters/LowpassFilter"; | |||||
| import { StereoWidthFilter } from "@/filters/StereoWidthFilter"; | |||||
| import { HighpassFilter } from "@/filters/HighpassFilter"; | |||||
| import * as Sources from "@/sources/PremadeSources"; | |||||
| @Options({ | @Options({ | ||||
| props: { | props: { | ||||
| @@ -42,24 +37,7 @@ export default class SoundscapeComp extends Vue { | |||||
| context!: AudioContext; | context!: AudioContext; | ||||
| mounted(): void { | mounted(): void { | ||||
| this.soundscape.addSource(Sources.makeGlorps()); | |||||
| this.soundscape.addSource(Sources.makeDigestion()); | |||||
| this.soundscape.addSource(Sources.makeBurps()); | |||||
| this.soundscape.addSource(Sources.makeGurgles()); | |||||
| const biquad: Filter = new BiquadFilter(); | |||||
| biquad.active = false; | |||||
| this.soundscape.addFilter(biquad); | |||||
| const stereo: Filter = new StereoWidthFilter(); | |||||
| stereo.active = false; | |||||
| this.soundscape.addFilter(stereo); | |||||
| const highpass: Filter = new HighpassFilter(); | |||||
| highpass.active = false; | |||||
| this.soundscape.addFilter(highpass); | |||||
| this.soundscape.start(); | this.soundscape.start(); | ||||
| console.log(this.soundscape); | |||||
| } | } | ||||
| } | } | ||||
| </script> | </script> | ||||
| @@ -68,6 +46,7 @@ export default class SoundscapeComp extends Vue { | |||||
| .soundscape { | .soundscape { | ||||
| margin: auto; | margin: auto; | ||||
| padding: 20px; | padding: 20px; | ||||
| margin: 20px; | |||||
| height: 100%; | height: 100%; | ||||
| display: grid; | display: grid; | ||||
| grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); | grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); | ||||
| @@ -1,6 +1,6 @@ | |||||
| import { Filter } from "./Filter"; | import { Filter } from "./Filter"; | ||||
| import { context, exposedNumber } from "../audio"; | import { context, exposedNumber } from "../audio"; | ||||
| export class BiquadFilter extends Filter { | |||||
| export class LowpassFilter extends Filter { | |||||
| public kind = "Biquad Filter"; | public kind = "Biquad Filter"; | ||||
| private biquad: BiquadFilterNode; | private biquad: BiquadFilterNode; | ||||
| @@ -20,6 +20,16 @@ export class IntervalSource extends Source { | |||||
| }) | }) | ||||
| public interval: [number, number] = [4, 6]; | public interval: [number, number] = [4, 6]; | ||||
| @exposedRange({ | |||||
| name: "Pitch", | |||||
| min: 0.25, | |||||
| max: 4, | |||||
| format: (value: number) => Math.round(Math.pow(4, value) * 100) + "%", | |||||
| map: (value: number) => Math.log(value) / Math.log(4), | |||||
| unmap: (value: number) => Math.pow(4, value), | |||||
| }) | |||||
| public pitch = [0.9, 1.1]; | |||||
| @exposedRange({ | @exposedRange({ | ||||
| name: "Left/Right Range", | name: "Left/Right Range", | ||||
| min: -1, | min: -1, | ||||
| @@ -74,7 +84,8 @@ export class IntervalSource extends Source { | |||||
| node.connect(pan); | node.connect(pan); | ||||
| pan.connect(this.gain); | pan.connect(this.gain); | ||||
| node.playbackRate.value = this.pitch; | |||||
| node.playbackRate.value = | |||||
| Math.random() * (this.pitch[1] - this.pitch[0]) + this.pitch[0]; | |||||
| node.start(); | node.start(); | ||||
| node.onended = () => { | node.onended = () => { | ||||
| @@ -1,5 +1,5 @@ | |||||
| import { Source } from "./Source"; | import { Source } from "./Source"; | ||||
| import { context } from "../audio"; | |||||
| import { context, exposedNumber } from "../audio"; | |||||
| export class LoopingSource extends Source { | export class LoopingSource extends Source { | ||||
| kind = "Looping"; | kind = "Looping"; | ||||
| @@ -7,6 +7,16 @@ export class LoopingSource extends Source { | |||||
| private started = false; | private started = false; | ||||
| private running = false; | private running = false; | ||||
| @exposedNumber({ | |||||
| name: "Pitch", | |||||
| min: 0.25, | |||||
| max: 4, | |||||
| format: (value: number) => Math.round(Math.pow(4, value) * 100) + "%", | |||||
| map: (value: number) => Math.log(value) / Math.log(4), | |||||
| unmap: (value: number) => Math.pow(4, value), | |||||
| }) | |||||
| public pitch = 1; | |||||
| constructor(name: string) { | constructor(name: string) { | ||||
| super(name); | super(name); | ||||
| } | } | ||||
| @@ -3,7 +3,7 @@ import { LoopingSource } from "./LoopingSource"; | |||||
| import { Source } from "./Source"; | import { Source } from "./Source"; | ||||
| export function makeGlorps(): Source { | export function makeGlorps(): Source { | ||||
| const source: Source = new IntervalSource("Guts"); | |||||
| const source: IntervalSource = new IntervalSource("Guts"); | |||||
| source.loadSound("bowels-to-intestines"); | source.loadSound("bowels-to-intestines"); | ||||
| source.loadSound("intestines-to-bowels"); | source.loadSound("intestines-to-bowels"); | ||||
| source.loadSound("intestines-to-stomach"); | source.loadSound("intestines-to-stomach"); | ||||
| @@ -16,20 +16,14 @@ export function makeGlorps(): Source { | |||||
| console.log(source); | console.log(source); | ||||
| return source; | |||||
| } | |||||
| export function makeDigestion(): Source { | |||||
| const source: Source = new LoopingSource("Digestion"); | |||||
| source.loadSound("fen-stomach"); | |||||
| source.loadSound("fen-intestines"); | |||||
| source.loadSound("fen-bowels"); | |||||
| source.interval = [4, 8]; | |||||
| source.pitch = [0.75, 1.25]; | |||||
| return source; | return source; | ||||
| } | } | ||||
| export function makeBurps(): Source { | export function makeBurps(): Source { | ||||
| const source: Source = new IntervalSource("Burps"); | |||||
| const source: IntervalSource = new IntervalSource("Burps"); | |||||
| source.loadSound("belch (1)"); | source.loadSound("belch (1)"); | ||||
| source.loadSound("belch (2)"); | source.loadSound("belch (2)"); | ||||
| source.loadSound("belch (3)"); | source.loadSound("belch (3)"); | ||||
| @@ -47,13 +41,17 @@ export function makeBurps(): Source { | |||||
| source.loadSound("belch (15)"); | source.loadSound("belch (15)"); | ||||
| source.loadSound("belch (16)"); | source.loadSound("belch (16)"); | ||||
| source.interval = [10, 30]; | |||||
| source.pitch = [0.8, 1.1]; | |||||
| source.active = false; | source.active = false; | ||||
| return source; | return source; | ||||
| } | } | ||||
| export function makeGurgles(): Source { | export function makeGurgles(): Source { | ||||
| const source: Source = new IntervalSource("Gurgles"); | |||||
| const source: IntervalSource = new IntervalSource("Gurgles"); | |||||
| source.loadSound("gurgles/gurgle (1)"); | source.loadSound("gurgles/gurgle (1)"); | ||||
| source.loadSound("gurgles/gurgle (2)"); | source.loadSound("gurgles/gurgle (2)"); | ||||
| source.loadSound("gurgles/gurgle (3)"); | source.loadSound("gurgles/gurgle (3)"); | ||||
| @@ -76,5 +74,41 @@ export function makeGurgles(): Source { | |||||
| source.loadSound("gurgles/gurgle (20)"); | source.loadSound("gurgles/gurgle (20)"); | ||||
| source.loadSound("gurgles/gurgle (21)"); | 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; | return source; | ||||
| } | } | ||||
| @@ -28,16 +28,6 @@ export abstract class Source extends Node { | |||||
| }) | }) | ||||
| public volume = 1; | public volume = 1; | ||||
| @exposedNumber({ | |||||
| name: "Pitch", | |||||
| min: 0.25, | |||||
| max: 4, | |||||
| format: (value: number) => Math.round(Math.pow(4, value) * 100) + "%", | |||||
| map: (value: number) => Math.log(value) / Math.log(4), | |||||
| unmap: (value: number) => Math.pow(4, value), | |||||
| }) | |||||
| public pitch = 1; | |||||
| constructor(name: string) { | constructor(name: string) { | ||||
| super(name); | super(name); | ||||
| this.gain = context.createGain(); | this.gain = context.createGain(); | ||||