| @@ -8,9 +8,13 @@ | |||
| "lint": "vue-cli-service lint" | |||
| }, | |||
| "dependencies": { | |||
| "@vueform/slider": "^2.0.5", | |||
| "core-js": "^3.6.5", | |||
| "postcss": "^8.3.6", | |||
| "reflect-metadata": "^0.1.13", | |||
| "vue": "^3.0.0", | |||
| "vue-class-component": "^8.0.0-0" | |||
| "vue-class-component": "^8.0.0-0", | |||
| "vue-slider-component": "^3.2.14" | |||
| }, | |||
| "devDependencies": { | |||
| "@typescript-eslint/eslint-plugin": "^4.18.0", | |||
| @@ -5,22 +5,30 @@ | |||
| <script lang="ts"> | |||
| import { Options, Vue } from "vue-class-component"; | |||
| import VoreAudio from "./components/VoreAudio.vue"; | |||
| import SourceNode from "./components/sources/SourceNode.vue"; | |||
| @Options({ | |||
| components: { | |||
| VoreAudio, | |||
| SourceNode, | |||
| }, | |||
| }) | |||
| export default class App extends Vue {} | |||
| </script> | |||
| <style> | |||
| body { | |||
| background: #111; | |||
| } | |||
| #app { | |||
| font-family: Avenir, Helvetica, Arial, sans-serif; | |||
| -webkit-font-smoothing: antialiased; | |||
| -moz-osx-font-smoothing: grayscale; | |||
| text-align: center; | |||
| color: #2c3e50; | |||
| color: #ddd; | |||
| background: #111; | |||
| margin-top: 60px; | |||
| } | |||
| </style> | |||
| <style src="@vueform/slider/themes/default.css"></style> | |||
| @@ -1,12 +1,57 @@ | |||
| export abstract class Source { | |||
| import "reflect-metadata"; | |||
| export abstract class Node { | |||
| constructor(public name: string) {} | |||
| } | |||
| export type NumberMetadata = { | |||
| name: string; | |||
| min: number; | |||
| max: number; | |||
| }; | |||
| export type RangeMetadata = { | |||
| name: string; | |||
| min: number; | |||
| max: number; | |||
| }; | |||
| export const exposedMetadataNumber = Symbol("exposedNumber"); | |||
| function exposedNumber(name: string, min: number, max: number) { | |||
| return Reflect.metadata(exposedMetadataNumber, { | |||
| name: name, | |||
| min: min, | |||
| max: max, | |||
| }); | |||
| } | |||
| export const exposedRangeMetadata = Symbol("exposedRange"); | |||
| function exposedRange(name: string, min: number, max: number) { | |||
| return Reflect.metadata(exposedRangeMetadata, { | |||
| name: name, | |||
| min: min, | |||
| max: max, | |||
| }); | |||
| } | |||
| export abstract class Source extends Node { | |||
| public abstract kind: string; | |||
| protected sounds: Array<AudioBuffer> = []; | |||
| public gain: GainNode; | |||
| public output: StereoPannerNode; | |||
| public output: GainNode; | |||
| @exposedNumber("Volume", 0, 1) | |||
| public volume = 1; | |||
| constructor(public name: string) { | |||
| @exposedRange("Panning", -1, 1) | |||
| public panning: [number, number] = [-0.2, 0.2]; | |||
| constructor(name: string) { | |||
| super(name); | |||
| this.gain = context.createGain(); | |||
| this.output = context.createStereoPanner(); | |||
| this.output = context.createGain(); | |||
| this.gain.connect(this.output); | |||
| } | |||
| @@ -20,17 +65,37 @@ export abstract class Source { | |||
| public abstract start(): void; | |||
| public abstract tick(dt: number): void; | |||
| public tick(dt: number): void { | |||
| this.gain.gain.value = this.volume; | |||
| } | |||
| } | |||
| export class IntervalSource extends Source { | |||
| kind = "Interval"; | |||
| private remaining: number; | |||
| @exposedRange("Interval", 0.25, 30) | |||
| public interval: [number, number] = [1, 5]; | |||
| private remaining = 0; | |||
| private started = false; | |||
| constructor(name: string, public interval: number, public randomness = 0) { | |||
| constructor( | |||
| name: string, | |||
| minTime: number, | |||
| maxTime: number, | |||
| public randomness = 0 | |||
| ) { | |||
| super(name); | |||
| this.remaining = this.interval + (Math.random() - 0.5) * 2 * this.randomness | |||
| this.interval = [minTime, maxTime]; | |||
| this.setTimer(); | |||
| } | |||
| private setTimer(): void { | |||
| this.remaining = this.interval[0]; | |||
| this.remaining += (this.interval[1] - this.interval[0]) * Math.random(); | |||
| this.remaining *= 1000; | |||
| } | |||
| public start(): void { | |||
| @@ -38,22 +103,33 @@ export class IntervalSource extends Source { | |||
| } | |||
| public tick(dt: number): void { | |||
| super.tick(dt); | |||
| if (this.started) { | |||
| this.remaining -= dt; | |||
| if (this.remaining <= 0) { | |||
| const index = Math.floor(Math.random() * this.sounds.length); | |||
| const node = context.createBufferSource(); | |||
| node.buffer = this.sounds[index]; | |||
| node.connect(this.gain); | |||
| const pan = context.createStereoPanner(); | |||
| pan.pan.value = | |||
| Math.random() * (this.panning[1] - this.panning[0]) + this.panning[0]; | |||
| node.connect(pan); | |||
| pan.connect(this.gain); | |||
| node.start(); | |||
| this.output.pan.value = Math.random() * 0.4 - 0.2; | |||
| this.remaining = this.interval + (Math.random() - 0.5) * 2 * this.randomness | |||
| node.onended = () => { | |||
| pan.disconnect(); | |||
| }; | |||
| this.setTimer(); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -69,15 +145,19 @@ export class LoopingSource extends Source { | |||
| public start(): void { | |||
| this.started = true; | |||
| } | |||
| private pickRandom(): void { | |||
| const index = Math.floor(Math.random() * this.sounds.length); | |||
| this.source = context.createBufferSource(); | |||
| this.source.buffer = this.sounds[index]; | |||
| this.source.connect(this.gain); | |||
| this.source.onended = () => { this.pickRandom(); this.source.start(); }; | |||
| this.source.onended = () => { | |||
| this.pickRandom(); | |||
| this.source.start(); | |||
| }; | |||
| } | |||
| public tick(dt: number) { | |||
| public tick(dt: number): void { | |||
| super.tick(dt); | |||
| if (this.started && this.sounds.length > 0 && !this.running) { | |||
| this.pickRandom(); | |||
| this.source.start(); | |||
| @@ -0,0 +1,102 @@ | |||
| <template> | |||
| <div class="node-props"> | |||
| <div | |||
| class="node-prop" | |||
| v-for="(metadata, index) in numberProps" | |||
| :key="index" | |||
| > | |||
| {{ metadata.name }} | |||
| <Slider | |||
| v-model="source[metadata.key]" | |||
| :min="metadata.min" | |||
| :max="metadata.max" | |||
| :step="-1" | |||
| :showTooltip="'drag'" | |||
| /> | |||
| </div> | |||
| <div class="node-prop" v-for="(metadata, index) in rangeProps" :key="index"> | |||
| {{ metadata.name }} | |||
| <Slider | |||
| v-model="source[metadata.key]" | |||
| :min="metadata.min" | |||
| :max="metadata.max" | |||
| :step="-1" | |||
| :showTooltip="'drag'" | |||
| /> | |||
| </div> | |||
| </div> | |||
| </template> | |||
| <script lang="ts"> | |||
| import { | |||
| exposedMetadataNumber, | |||
| exposedRangeMetadata, | |||
| NumberMetadata, | |||
| RangeMetadata, | |||
| Source, | |||
| } from "@/audio"; | |||
| import { Options, Vue } from "vue-class-component"; | |||
| import Slider from "@vueform/slider"; | |||
| @Options({ | |||
| props: { | |||
| source: Source, | |||
| }, | |||
| components: { | |||
| Slider, | |||
| }, | |||
| }) | |||
| export default class NodeProps extends Vue { | |||
| source!: Source; | |||
| numberProps: Array<{ name: string; key: string; min: number; max: number }> = | |||
| []; | |||
| rangeProps: Array<{ name: string; key: string; min: number; max: number }> = | |||
| []; | |||
| mounted(): void { | |||
| Object.keys(this.source).forEach((key) => { | |||
| const metadata: NumberMetadata | undefined = Reflect.getMetadata( | |||
| exposedMetadataNumber, | |||
| this.source, | |||
| key | |||
| ); | |||
| if (metadata !== undefined) { | |||
| this.numberProps.push({ | |||
| name: metadata.name, | |||
| key: key, | |||
| min: metadata.min, | |||
| max: metadata.max, | |||
| }); | |||
| } | |||
| }); | |||
| Object.keys(this.source).forEach((key) => { | |||
| const metadata: RangeMetadata | undefined = Reflect.getMetadata( | |||
| exposedRangeMetadata, | |||
| this.source, | |||
| key | |||
| ); | |||
| if (metadata !== undefined) { | |||
| this.rangeProps.push({ | |||
| name: metadata.name, | |||
| key: key, | |||
| min: metadata.min, | |||
| max: metadata.max, | |||
| }); | |||
| } | |||
| }); | |||
| console.log(this.numberProps[0]); | |||
| console.log(this.rangeProps[0]); | |||
| } | |||
| } | |||
| </script> | |||
| <style scoped> | |||
| .node-prop { | |||
| margin: 20px; | |||
| user-select: none; | |||
| } | |||
| </style> | |||
| @@ -3,30 +3,54 @@ | |||
| <div>This is a mega-early-alpha vore audio generator.</div> | |||
| <div>Click the buttons below to start each kind of audio.</div> | |||
| <div>(clicking repeatedly will play even MORE audio)</div> | |||
| <div>Follow <a href="https://twitter.com/causticcrux">@causticcrux</a> for more.</div> | |||
| <div>Many sounds by <a href="https://www.furaffinity.net/user/jeschke">Jit</a>!</div> | |||
| <div> | |||
| Follow <a href="https://twitter.com/causticcrux">@causticcrux</a> for more. | |||
| </div> | |||
| <div> | |||
| Many sounds by <a href="https://www.furaffinity.net/user/jeschke">Jit</a>! | |||
| </div> | |||
| <button v-on:click="startGlorps">Glorps</button> | |||
| <button v-on:click="startDigestion">Digestion</button> | |||
| <button v-on:click="startBurps">Burps</button> | |||
| <button v-on:click="startGurgles">Gurgles</button> | |||
| <div class="soundscape"> | |||
| <source-node | |||
| v-for="(source, index) in sources" | |||
| :key="index" | |||
| :source="source" | |||
| > | |||
| </source-node> | |||
| </div> | |||
| <div></div> | |||
| <button v-on:click="clear">Delete all cached sound (if it gets stuck)</button> | |||
| </template> | |||
| <script lang="ts"> | |||
| import { clearCache, IntervalSource, LoopingSource, setup, Source } from "@/audio"; | |||
| import { | |||
| clearCache, | |||
| IntervalSource, | |||
| LoopingSource, | |||
| setup, | |||
| Source, | |||
| } from "@/audio"; | |||
| import { Options, Vue } from "vue-class-component"; | |||
| import SourceNode from "./sources/SourceNode.vue"; | |||
| @Options({ | |||
| props: { | |||
| msg: String, | |||
| }, | |||
| components: { | |||
| SourceNode, | |||
| }, | |||
| }) | |||
| export default class VoreAudio extends Vue { | |||
| context!: AudioContext; | |||
| sources: Array<Source> = []; | |||
| startGlorps(): void { | |||
| const source: Source = new IntervalSource("Guts", 5000, 2000); | |||
| const source: Source = new IntervalSource("Guts", 5, 8); | |||
| source.loadSound("bowels-to-intestines.ogg"); | |||
| source.loadSound("intestines-to-bowels.ogg"); | |||
| source.loadSound("intestines-to-stomach.ogg"); | |||
| @@ -37,9 +61,10 @@ export default class VoreAudio extends Vue { | |||
| source.loadSound("bowels-churn-safe.ogg"); | |||
| source.loadSound("bowels-churn-danger.ogg"); | |||
| source.output.connect(this.context.destination); | |||
| console.log(source) | |||
| source.start(); | |||
| setInterval(() => source.tick(100), 100); | |||
| this.sources.push(source); | |||
| } | |||
| startDigestion(): void { | |||
| @@ -51,10 +76,12 @@ export default class VoreAudio extends Vue { | |||
| source.start(); | |||
| console.log(source); | |||
| setInterval(() => source.tick(100), 100); | |||
| this.sources.push(source); | |||
| } | |||
| startBurps(): void { | |||
| const source: Source = new IntervalSource("Burps", 12000, 3000); | |||
| const source: Source = new IntervalSource("Burps", 5, 15); | |||
| source.loadSound("belch (1).ogg"); | |||
| source.loadSound("belch (2).ogg"); | |||
| source.loadSound("belch (3).ogg"); | |||
| @@ -75,10 +102,12 @@ export default class VoreAudio extends Vue { | |||
| source.start(); | |||
| console.log(source); | |||
| setInterval(() => source.tick(100), 100); | |||
| this.sources.push(source); | |||
| } | |||
| startGurgles(): void { | |||
| const source: Source = new IntervalSource("Gurgles", 3000, 1000); | |||
| const source: Source = new IntervalSource("Gurgles", 3, 10); | |||
| source.loadSound("gurgles/gurgle (1).ogg"); | |||
| source.loadSound("gurgles/gurgle (2).ogg"); | |||
| source.loadSound("gurgles/gurgle (3).ogg"); | |||
| @@ -105,9 +134,11 @@ export default class VoreAudio extends Vue { | |||
| source.gain.gain.value = 0.5; | |||
| console.log(source); | |||
| setInterval(() => source.tick(100), 100); | |||
| this.sources.push(source); | |||
| } | |||
| clear() { | |||
| clear(): void { | |||
| clearCache(); | |||
| } | |||
| @@ -118,18 +149,15 @@ export default class VoreAudio extends Vue { | |||
| </script> | |||
| <style scoped> | |||
| h3 { | |||
| margin: 40px 0 0; | |||
| } | |||
| ul { | |||
| list-style-type: none; | |||
| padding: 0; | |||
| } | |||
| li { | |||
| display: inline-block; | |||
| margin: 0 10px; | |||
| } | |||
| a { | |||
| color: #42b983; | |||
| .soundscape { | |||
| margin: auto; | |||
| padding: 20px; | |||
| width: 50vw; | |||
| min-width: 1000px; | |||
| height: 100%; | |||
| display: grid; | |||
| grid-template-columns: 1fr 1fr; | |||
| grid-auto-rows: 200px; | |||
| grid-gap: 20px; | |||
| } | |||
| </style> | |||
| @@ -0,0 +1,45 @@ | |||
| <template> | |||
| <div class="source-node"> | |||
| <div class="node-name">{{ source.name }}</div> | |||
| <node-props :source="source"></node-props> | |||
| </div> | |||
| </template> | |||
| <script lang="ts"> | |||
| import { Source } from "@/audio"; | |||
| import { Options, Vue } from "vue-class-component"; | |||
| import NodeProps from "@/components/NodeProps.vue"; | |||
| @Options({ | |||
| props: { | |||
| source: Source, | |||
| }, | |||
| components: { | |||
| NodeProps, | |||
| }, | |||
| }) | |||
| export default class SourceNode extends Vue { | |||
| source!: Source; | |||
| } | |||
| </script> | |||
| <style scoped> | |||
| .source-node { | |||
| width: 100%; | |||
| height: 100%; | |||
| background: gray; | |||
| display: flex; | |||
| flex-direction: column; | |||
| } | |||
| .node-name { | |||
| font-size: 24pt; | |||
| margin: 4pt; | |||
| color: #fcf; | |||
| } | |||
| .node-properties { | |||
| display: flex; | |||
| flex-direction: column; | |||
| } | |||
| </style> | |||