소스 검색

Add filter nodes

master
Fen Dweller 4 년 전
부모
커밋
33df50550c
11개의 변경된 파일370개의 추가작업 그리고 198개의 파일을 삭제
  1. +0
    -2
      src/App.vue
  2. +7
    -148
      src/audio.ts
  3. +9
    -12
      src/components/NodeProps.vue
  4. +66
    -33
      src/components/VoreAudio.vue
  5. +61
    -0
      src/components/nodes/FilterNode.vue
  6. +3
    -3
      src/components/nodes/SourceNode.vue
  7. +52
    -0
      src/filters/Filter.ts
  8. +23
    -0
      src/filters/LowpassFilter.ts
  9. +68
    -0
      src/sources/IntervalSource.ts
  10. +35
    -0
      src/sources/LoopingSource.ts
  11. +46
    -0
      src/sources/Source.ts

+ 0
- 2
src/App.vue 파일 보기

@@ -5,12 +5,10 @@
<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 {}


+ 7
- 148
src/audio.ts 파일 보기

@@ -1,4 +1,5 @@
import "reflect-metadata";
import Source from "./sources/Source";

export abstract class Node {
constructor(public name: string) {}
@@ -18,7 +19,8 @@ export type RangeMetadata = {

export const exposedMetadataNumber = Symbol("exposedNumber");

function exposedNumber(name: string, min: number, max: number) {
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function exposedNumber(name: string, min: number, max: number) {
return Reflect.metadata(exposedMetadataNumber, {
name: name,
min: min,
@@ -28,7 +30,8 @@ function exposedNumber(name: string, min: number, max: number) {

export const exposedRangeMetadata = Symbol("exposedRange");

function exposedRange(name: string, min: number, max: number) {
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function exposedRange(name: string, min: number, max: number) {
return Reflect.metadata(exposedRangeMetadata, {
name: name,
min: min,
@@ -36,151 +39,7 @@ function exposedRange(name: string, min: number, max: number) {
});
}

export abstract class Source extends Node {
public abstract kind: string;
protected sounds: Array<AudioBuffer> = [];
public gain: GainNode;
public output: GainNode;
public _active = true;

get active(): boolean {
return this._active;
}

set active(value: boolean) {
this._active = value;

this.output.gain.linearRampToValueAtTime(
value ? 1.0 : 0.0,
context.currentTime + 0.5
);
}

@exposedNumber("Volume", 0, 1)
public volume = 1;

constructor(name: string) {
super(name);
this.gain = context.createGain();
this.output = context.createGain();
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 tick(dt: number): void {
this.gain.gain.value = this.volume;
}
}

export class IntervalSource extends Source {
kind = "Interval";

@exposedRange("Interval", 0.25, 30)
public interval: [number, number] = [1, 5];

@exposedRange("Panning", -1, 1)
public panning: [number, number] = [-0.2, 0.2];

private remaining = 0;

private started = false;
constructor(
name: string,
minTime: number,
maxTime: number,
public randomness = 0
) {
super(name);

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 {
this.started = true;
}

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];

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

node.onended = () => {
pan.disconnect();
};

this.setTimer();
}
}
}
}

export class LoopingSource extends Source {
kind = "Looping";
private source!: AudioBufferSourceNode;
private started = false;
private running = false;
constructor(name: string) {
super(name);
}

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();
};
}
public tick(dt: number): void {
super.tick(dt);
if (this.started && this.sounds.length > 0 && !this.running) {
this.pickRandom();
this.source.start();
this.running = true;
}
}
}

let context: AudioContext;
export let context: AudioContext;

const audioBaseUrl = "/audio/";

@@ -189,7 +48,7 @@ const audioDict: Map<string, AudioBuffer> = new Map();

// asynchronously load an audio file

function loadAudio(name: string, source: Source, flush = false) {
export function loadAudio(name: string, source: Source, flush = false) {
// are we already trying to get the audio?

if (!waiting.has(name)) {


+ 9
- 12
src/components/NodeProps.vue 파일 보기

@@ -7,7 +7,7 @@
>
{{ metadata.name }}
<Slider
v-model="source[metadata.key]"
v-model="node[metadata.key]"
:min="metadata.min"
:max="metadata.max"
:step="-1"
@@ -17,7 +17,7 @@
<div class="node-prop" v-for="(metadata, index) in rangeProps" :key="index">
{{ metadata.name }}
<Slider
v-model="source[metadata.key]"
v-model="node[metadata.key]"
:min="metadata.min"
:max="metadata.max"
:step="-1"
@@ -33,31 +33,31 @@ import {
exposedRangeMetadata,
NumberMetadata,
RangeMetadata,
Source,
} from "@/audio";
import { Options, Vue } from "vue-class-component";
import Slider from "@vueform/slider";
import { Node } from "@/audio";

@Options({
props: {
source: Source,
node: Node,
},
components: {
Slider,
},
})
export default class NodeProps extends Vue {
source!: Source;
node!: Node;
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) => {
Object.keys(this.node).forEach((key) => {
const metadata: NumberMetadata | undefined = Reflect.getMetadata(
exposedMetadataNumber,
this.source,
this.node,
key
);

@@ -71,10 +71,10 @@ export default class NodeProps extends Vue {
}
});

Object.keys(this.source).forEach((key) => {
Object.keys(this.node).forEach((key) => {
const metadata: RangeMetadata | undefined = Reflect.getMetadata(
exposedRangeMetadata,
this.source,
this.node,
key
);

@@ -87,9 +87,6 @@ export default class NodeProps extends Vue {
});
}
});

console.log(this.numberProps[0]);
console.log(this.rangeProps[0]);
}
}
</script>


+ 66
- 33
src/components/VoreAudio.vue 파일 보기

@@ -9,7 +9,7 @@
<div>
Many sounds by <a href="https://www.furaffinity.net/user/jeschke">Jit</a>!
</div>
<button v-on:click="start">Start</button>
<button v-on:click="start" class="start-button" v-if="!started">Start</button>
<div class="soundscape">
<source-node
v-for="(source, index) in sources"
@@ -17,6 +17,12 @@
:source="source"
>
</source-node>
<filter-node
v-for="(filter, index) in filters"
:key="index"
:filter="filter"
>
</filter-node>
</div>
<div></div>

@@ -24,15 +30,15 @@
</template>

<script lang="ts">
import {
clearCache,
IntervalSource,
LoopingSource,
setup,
Source,
} from "@/audio";
import { clearCache, setup } from "@/audio";
import { Options, Vue } from "vue-class-component";
import SourceNode from "./sources/SourceNode.vue";
import Source from "@/sources/Source";
import SourceNode from "./nodes/SourceNode.vue";
import FilterNode from "./nodes/FilterNode.vue";
import LoopingSource from "@/sources/LoopingSource";
import IntervalSource from "@/sources/IntervalSource";
import Filter from "@/filters/Filter";
import BiquadFilter from "@/filters/LowpassFilter";

@Options({
props: {
@@ -40,11 +46,37 @@ import SourceNode from "./sources/SourceNode.vue";
},
components: {
SourceNode,
FilterNode,
},
})
export default class VoreAudio extends Vue {
started = false;
context!: AudioContext;
sources: Array<Source> = [];
filters: Array<Filter> = [];
filterBus!: GainNode;

addSource(source: Source): void {
source.output.connect(this.filterBus);
this.sources.push(source);
source.start();
}

addFilter(filter: Filter): void {
if (this.filters.length > 0) {
const last: Filter = this.filters[this.filters.length - 1];
last.output.disconnect();
last.output.connect(filter.input);
filter.output.connect(this.context.destination);
} else {
this.filterBus.disconnect();
this.filterBus.connect(filter.input);
filter.output.connect(this.context.destination);
}

filter.start();
this.filters.push(filter);
}

startGlorps(): void {
const source: Source = new IntervalSource("Guts", 5, 8);
@@ -57,13 +89,9 @@ export default class VoreAudio extends Vue {
source.loadSound("stomach-churn.ogg");
source.loadSound("bowels-churn-safe.ogg");
source.loadSound("bowels-churn-danger.ogg");
source.output.connect(this.context.destination);
source.start();
setInterval(() => source.tick(100), 100);

source.active = false;

this.sources.push(source);
this.addSource(source);
}

startDigestion(): void {
@@ -71,12 +99,8 @@ export default class VoreAudio extends Vue {
source.loadSound("fen-stomach.ogg");
source.loadSound("fen-intestines.ogg");
source.loadSound("fen-bowels.ogg");
source.output.connect(this.context.destination);
source.start();
console.log(source);
setInterval(() => source.tick(100), 100);

this.sources.push(source);
this.addSource(source);
}

startBurps(): void {
@@ -97,14 +121,10 @@ export default class VoreAudio extends Vue {
source.loadSound("belch (14).ogg");
source.loadSound("belch (15).ogg");
source.loadSound("belch (16).ogg");
source.output.connect(this.context.destination);
source.start();
console.log(source);
setInterval(() => source.tick(100), 100);

source.active = false;

this.sources.push(source);
this.addSource(source);
}

startGurgles(): void {
@@ -130,13 +150,8 @@ export default class VoreAudio extends Vue {
source.loadSound("gurgles/gurgle (19).ogg");
source.loadSound("gurgles/gurgle (20).ogg");
source.loadSound("gurgles/gurgle (21).ogg");
source.output.connect(this.context.destination);
source.start();
source.volume = 0.5;
console.log(source);
setInterval(() => source.tick(100), 100);

this.sources.push(source);
this.addSource(source);
}

clear(): void {
@@ -144,14 +159,29 @@ export default class VoreAudio extends Vue {
}

start(): void {
if (this.started) {
return;
}

this.started = true;

this.startGlorps();
this.startGurgles();
this.startDigestion();
this.startBurps();
const filter: Filter = new BiquadFilter();
filter.active = false;
this.addFilter(filter);
setInterval(() => {
this.sources.forEach((source) => source.tick(100));
this.filters.forEach((filter) => filter.tick(100));
}, 100);
}

mounted(): void {
this.context = setup();
this.filterBus = this.context.createGain();
this.filterBus.connect(this.context.destination);
}
}
</script>
@@ -160,12 +190,15 @@ export default class VoreAudio extends Vue {
.soundscape {
margin: auto;
padding: 20px;
width: 50vw;
min-width: 1000px;
width: minmax(50vw, 1500px);
height: 100%;
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
grid-auto-rows: 200px;
grid-gap: 20px;
}

.start-button {
font-size: 60pt;
}
</style>

+ 61
- 0
src/components/nodes/FilterNode.vue 파일 보기

@@ -0,0 +1,61 @@
<template>
<div :class="filter.active ? '' : 'inactive'" class="filter-node">
<Toggle class="active-toggle" v-model="filter.active" />
<div class="node-name">{{ filter.name }}</div>
<node-props :node="filter"></node-props>
</div>
</template>

<script lang="ts">
import Filter from "@/filters/Filter";

import { Options, Vue } from "vue-class-component";
import NodeProps from "@/components/NodeProps.vue";
import Toggle from "@vueform/toggle";

@Options({
props: {
filter: Filter,
},
components: {
NodeProps,
Toggle,
},
})
export default class FilterNode extends Vue {
filter!: Filter;
}
</script>

<style scoped>
.filter-node {
width: 100%;
height: 100%;
background: #333;
display: flex;
flex-direction: column;
position: relative;
transition: 0.2s background;
}

.filter-node.inactive {
background: #555;
}

.node-name {
font-size: 24pt;
margin: 4pt;
color: #fcf;
}

.node-properties {
display: flex;
flex-direction: column;
}

.active-toggle {
position: absolute;
top: 10px;
left: 10px;
}
</style>

src/components/sources/SourceNode.vue → src/components/nodes/SourceNode.vue 파일 보기

@@ -2,12 +2,12 @@
<div :class="source.active ? '' : 'inactive'" class="source-node">
<Toggle class="active-toggle" v-model="source.active" />
<div class="node-name">{{ source.name }}</div>
<node-props :source="source"></node-props>
<node-props :node="source"></node-props>
</div>
</template>

<script lang="ts">
import { Source } from "@/audio";
import Source from "@/sources/Source";
import { Options, Vue } from "vue-class-component";
import NodeProps from "@/components/NodeProps.vue";
import Toggle from "@vueform/toggle";
@@ -15,7 +15,7 @@ import Toggle from "@vueform/toggle";
@Options({
props: {
source: Source,
},
},
components: {
NodeProps,
Toggle,

+ 52
- 0
src/filters/Filter.ts 파일 보기

@@ -0,0 +1,52 @@
import { Node, context } from "../audio";

export default abstract class Filter extends Node {
public abstract kind: string;
public input: GainNode;
protected filterInput: GainNode;
protected bypass: GainNode;
public output: GainNode;
private started = false;
public _active = true;

get active(): boolean {
return this._active;
}

set active(value: boolean) {
this._active = value;

if (this.started) {
this.bypass.gain.setTargetAtTime(value ? 0 : 1, context.currentTime, 0.3);
this.filterInput.gain.setTargetAtTime(
value ? 1 : 0,
context.currentTime,
0.3
);
} else {
this.bypass.gain.value = value ? 0 : 1;
this.filterInput.gain.value = value ? 1 : 0;
}
}

constructor(name: string) {
super(name);
this.input = context.createGain();
this.filterInput = context.createGain();
this.bypass = context.createGain();
this.output = context.createGain();

this.input.connect(this.filterInput);
this.input.connect(this.bypass);
this.bypass.connect(this.output);
this.active = true;
}

public start(): void {
this.started = true;
}

public tick(dt: number): void {
/* */
}
}

+ 23
- 0
src/filters/LowpassFilter.ts 파일 보기

@@ -0,0 +1,23 @@
import Filter from "./Filter";
import { context, exposedNumber } from "../audio";
export default class BiquadFilter extends Filter {
public kind = "Biquad Filter";
private biquad: BiquadFilterNode;

@exposedNumber("Cutoff", 10, 10000)
public cutoff = 1000;

constructor() {
super("Lowpass Filter");
this.biquad = context.createBiquadFilter();

this.biquad.frequency.value = 100;
this.filterInput.connect(this.biquad);
this.biquad.connect(this.output);
}

public tick(dt: number): void {
super.tick(dt);
this.biquad.frequency.value = this.cutoff;
}
}

+ 68
- 0
src/sources/IntervalSource.ts 파일 보기

@@ -0,0 +1,68 @@
import Source from "./Source";
import { exposedRange, context } from "../audio";

export default class IntervalSource extends Source {
kind = "Interval";

@exposedRange("Interval", 0.25, 30)
public interval: [number, number] = [1, 5];

@exposedRange("Panning", -1, 1)
public panning: [number, number] = [-0.2, 0.2];

private remaining = 0;

private started = false;
constructor(
name: string,
minTime: number,
maxTime: number,
public randomness = 0
) {
super(name);

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 {
this.started = true;
}

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];

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

node.onended = () => {
pan.disconnect();
};

this.setTimer();
}
}
}
}

+ 35
- 0
src/sources/LoopingSource.ts 파일 보기

@@ -0,0 +1,35 @@
import Source from "./Source";
import { context } from "../audio";

export default class LoopingSource extends Source {
kind = "Looping";
private source!: AudioBufferSourceNode;
private started = false;
private running = false;
constructor(name: string) {
super(name);
}

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();
};
}
public tick(dt: number): void {
super.tick(dt);
if (this.started && this.sounds.length > 0 && !this.running) {
this.pickRandom();
this.source.start();
this.running = true;
}
}
}

+ 46
- 0
src/sources/Source.ts 파일 보기

@@ -0,0 +1,46 @@
import { Node, context, exposedNumber, loadAudio } from "../audio";

export default abstract class Source extends Node {
public abstract kind: string;
protected sounds: Array<AudioBuffer> = [];
public gain: GainNode;
public output: GainNode;
public _active = true;

get active(): boolean {
return this._active;
}

set active(value: boolean) {
this._active = value;

this.output.gain.linearRampToValueAtTime(
value ? 1.0 : 0.0,
context.currentTime + 0.5
);
}

@exposedNumber("Volume", 0, 1)
public volume = 1;

constructor(name: string) {
super(name);
this.gain = context.createGain();
this.output = context.createGain();
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 tick(dt: number): void {
this.gain.gain.value = this.volume;
}
}

불러오는 중...
취소
저장