浏览代码

Visualize the sources. Add configurable properties to nodes.

master
Fen Dweller 4 年前
父节点
当前提交
f86dbd6064
共有 7 个文件被更改,包括 1500 次插入84 次删除
  1. +1192
    -43
      package-lock.json
  2. +5
    -1
      package.json
  3. +9
    -1
      src/App.vue
  4. +98
    -18
      src/audio.ts
  5. +102
    -0
      src/components/NodeProps.vue
  6. +49
    -21
      src/components/VoreAudio.vue
  7. +45
    -0
      src/components/sources/SourceNode.vue

+ 1192
- 43
package-lock.json
文件差异内容过多而无法显示
查看文件


+ 5
- 1
package.json 查看文件

@@ -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",


+ 9
- 1
src/App.vue 查看文件

@@ -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>

+ 98
- 18
src/audio.ts 查看文件

@@ -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();


+ 102
- 0
src/components/NodeProps.vue 查看文件

@@ -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>

+ 49
- 21
src/components/VoreAudio.vue 查看文件

@@ -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>

+ 45
- 0
src/components/sources/SourceNode.vue 查看文件

@@ -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>

正在加载...
取消
保存