| @@ -48,18 +48,24 @@ export abstract class Node { | |||
| constructor(public name: string) {} | |||
| } | |||
| export type NumberMetadata = { | |||
| export type PropMetadata = { | |||
| name: string; | |||
| }; | |||
| export type NumberMetadata = PropMetadata & { | |||
| min: number; | |||
| max: number; | |||
| format?: (value: number) => string; | |||
| map?: (value: number) => number; | |||
| unmap?: (value: number) => number; | |||
| }; | |||
| export type RangeMetadata = { | |||
| name: string; | |||
| export type RangeMetadata = PropMetadata & { | |||
| min: number; | |||
| max: number; | |||
| format?: (value: number) => string; | |||
| map?: (value: number) => number; | |||
| unmap?: (value: number) => number; | |||
| }; | |||
| export const exposedMetadataNumber = Symbol("exposedNumber"); | |||
| @@ -0,0 +1,74 @@ | |||
| <template> | |||
| <div class="node-prop"> | |||
| <div class="prop-name"> | |||
| {{ metadata.name }} - {{ metadata.format(mappedValue) }} | |||
| </div> | |||
| <Slider | |||
| v-model="mappedValue" | |||
| :min="metadata.map ? metadata.map(metadata.min) : metadata.min" | |||
| :max="metadata.map ? metadata.map(metadata.max) : metadata.max" | |||
| :step="-1" | |||
| :showTooltip="'drag'" | |||
| :format="metadata.format" | |||
| :options="test" | |||
| /> | |||
| </div> | |||
| </template> | |||
| <script lang="ts"> | |||
| import { NumberMetadata } from "@/audio"; | |||
| import { Options, Vue } from "vue-class-component"; | |||
| import Slider from "@vueform/slider"; | |||
| import { Node } from "@/audio"; | |||
| @Options({ | |||
| props: { | |||
| node: Node, | |||
| propKey: String, | |||
| metadata: {}, | |||
| }, | |||
| components: { | |||
| Slider, | |||
| }, | |||
| }) | |||
| export default class NodeNumberProp extends Vue { | |||
| propKey!: string; | |||
| type!: string; | |||
| node!: Record<string, number>; | |||
| metadata!: NumberMetadata & { key: string }; | |||
| // it still animates ????? | |||
| // this fixes logarithmic sliders | |||
| // Why???? | |||
| // ??????????? | |||
| // ??????????????????? | |||
| test = { animate: false }; | |||
| get mappedValue(): number { | |||
| let result: number = this.node[this.metadata.key]; | |||
| if (this.metadata.map) { | |||
| result = this.metadata.map(result); | |||
| } | |||
| return result; | |||
| } | |||
| set mappedValue(value: number) { | |||
| if (this.metadata.unmap) { | |||
| value = this.metadata.unmap(value); | |||
| } | |||
| this.node[this.metadata.key] = value; | |||
| } | |||
| } | |||
| </script> | |||
| <style scoped> | |||
| .node-prop { | |||
| margin: 20px; | |||
| user-select: none; | |||
| } | |||
| .prop-name { | |||
| font-size: 150%; | |||
| margin-bottom: 4px; | |||
| } | |||
| </style> | |||
| @@ -1,31 +1,17 @@ | |||
| <template> | |||
| <div class="node-props"> | |||
| <div | |||
| class="node-prop" | |||
| <node-number-prop | |||
| v-for="(metadata, index) in numberProps" | |||
| :node="node" | |||
| :key="index" | |||
| > | |||
| <div class="prop-name">{{ metadata.name }}</div> | |||
| <Slider | |||
| v-model="node[metadata.key]" | |||
| :min="metadata.min" | |||
| :max="metadata.max" | |||
| :step="-1" | |||
| :showTooltip="'drag'" | |||
| :format="metadata.format" | |||
| /> | |||
| </div> | |||
| <div class="node-prop" v-for="(metadata, index) in rangeProps" :key="index"> | |||
| <div class="prop-name">{{ metadata.name }}</div> | |||
| <Slider | |||
| v-model="node[metadata.key]" | |||
| :min="metadata.min" | |||
| :max="metadata.max" | |||
| :step="-1" | |||
| :showTooltip="'drag'" | |||
| :format="metadata.format" | |||
| /> | |||
| </div> | |||
| :metadata="metadata" | |||
| /> | |||
| <node-range-prop | |||
| v-for="(metadata, index) in rangeProps" | |||
| :node="node" | |||
| :key="index" | |||
| :metadata="metadata" | |||
| /> | |||
| </div> | |||
| </template> | |||
| @@ -39,6 +25,8 @@ import { | |||
| import { Options, Vue } from "vue-class-component"; | |||
| import Slider from "@vueform/slider"; | |||
| import { Node } from "@/audio"; | |||
| import NodeNumberProp from "@/components/NodeNumberProp.vue"; | |||
| import NodeRangeProp from "@/components/NodeRangeProp.vue"; | |||
| @Options({ | |||
| props: { | |||
| @@ -46,6 +34,8 @@ import { Node } from "@/audio"; | |||
| }, | |||
| components: { | |||
| Slider, | |||
| NodeNumberProp, | |||
| NodeRangeProp, | |||
| }, | |||
| }) | |||
| export default class NodeProps extends Vue { | |||
| @@ -0,0 +1,80 @@ | |||
| <template> | |||
| <div class="node-prop"> | |||
| <div class="prop-name"> | |||
| {{ metadata.name }} - {{ metadata.format(mappedValue[0]) }}-{{ | |||
| metadata.format(mappedValue[1]) | |||
| }} | |||
| </div> | |||
| <Slider | |||
| v-model="mappedValue" | |||
| :min="metadata.map ? metadata.map(metadata.min) : metadata.min" | |||
| :max="metadata.map ? metadata.map(metadata.max) : metadata.max" | |||
| :step="-1" | |||
| :showTooltip="'drag'" | |||
| :format="metadata.format" | |||
| :options="test" | |||
| /> | |||
| </div> | |||
| </template> | |||
| <script lang="ts"> | |||
| import { NumberMetadata } from "@/audio"; | |||
| import { Options, Vue } from "vue-class-component"; | |||
| import Slider from "@vueform/slider"; | |||
| import { Node } from "@/audio"; | |||
| @Options({ | |||
| props: { | |||
| node: Node, | |||
| propKey: String, | |||
| metadata: {}, | |||
| }, | |||
| components: { | |||
| Slider, | |||
| }, | |||
| }) | |||
| export default class NodeRangeProp extends Vue { | |||
| propKey!: string; | |||
| type!: string; | |||
| node!: Record<string, [number, number]>; | |||
| metadata!: NumberMetadata & { key: string }; | |||
| // it still animates ????? | |||
| // this fixes logarithmic sliders | |||
| // Why???? | |||
| // ??????????? | |||
| // ??????????????????? | |||
| test = { animate: false }; | |||
| get mappedValue(): [number, number] { | |||
| let result: [number, number] = this.node[this.metadata.key]; | |||
| if (this.metadata.map) { | |||
| result = [this.metadata.map(result[0]), this.metadata.map(result[1])]; | |||
| } | |||
| return result; | |||
| } | |||
| set mappedValue(value: [number, number]) { | |||
| if (this.metadata.unmap) { | |||
| value = [this.metadata.unmap(value[0]), this.metadata.unmap(value[1])]; | |||
| } | |||
| const old = this.node[this.metadata.key]; | |||
| // this was causing an infinite loop | |||
| if (old[0] != value[0] || old[1] != value[1]) | |||
| this.node[this.metadata.key] = [value[0], value[1]]; | |||
| } | |||
| } | |||
| </script> | |||
| <style scoped> | |||
| .node-prop { | |||
| margin: 20px; | |||
| user-select: none; | |||
| } | |||
| .prop-name { | |||
| font-size: 150%; | |||
| margin-bottom: 4px; | |||
| } | |||
| </style> | |||
| @@ -8,7 +8,12 @@ export class HighpassFilter extends Filter { | |||
| name: "Cutoff Frequency", | |||
| min: 10, | |||
| max: 10000, | |||
| format: (value: number) => value + "Hz", | |||
| format: (value: number) => | |||
| Math.pow(2, value).toLocaleString(undefined, { | |||
| maximumFractionDigits: 0, | |||
| }) + "Hz", | |||
| map: (value: number) => Math.log(value) / Math.log(2), | |||
| unmap: (value: number) => Math.pow(2, value), | |||
| }) | |||
| public cutoff = 500; | |||
| @@ -8,7 +8,12 @@ export class BiquadFilter extends Filter { | |||
| name: "Cutoff Frequency", | |||
| min: 10, | |||
| max: 10000, | |||
| format: (value: number) => value + "Hz", | |||
| format: (value: number) => | |||
| Math.pow(2, value).toLocaleString(undefined, { | |||
| maximumFractionDigits: 0, | |||
| }) + "Hz", | |||
| map: (value: number) => Math.log(value) / Math.log(2), | |||
| unmap: (value: number) => Math.pow(2, value), | |||
| }) | |||
| public cutoff = 1000; | |||
| @@ -1,29 +1,22 @@ | |||
| import { Source } from "./Source"; | |||
| import { exposedNumber, context, exposedRange } from "../audio"; | |||
| import { context, exposedRange } from "../audio"; | |||
| export class IntervalSource extends Source { | |||
| kind = "Interval"; | |||
| @exposedNumber({ | |||
| name: "Pitch", | |||
| min: 0.25, | |||
| max: 4, | |||
| format: (value: number) => Math.round(value * 100) + "%", | |||
| }) | |||
| public pitch = 1; | |||
| // | |||
| @exposedNumber({ | |||
| @exposedRange({ | |||
| name: "Interval", | |||
| min: 0.25, | |||
| max: 30, | |||
| max: 300, | |||
| format: (value: number) => { | |||
| return ( | |||
| value.toLocaleString(undefined, { | |||
| Math.pow(2, value).toLocaleString(undefined, { | |||
| maximumFractionDigits: 2, | |||
| }) + "s" | |||
| ); | |||
| }, | |||
| map: (value: number) => Math.log(value) / Math.log(2), | |||
| unmap: (value: number) => Math.pow(2, value), | |||
| }) | |||
| public interval: [number, number] = [4, 6]; | |||
| @@ -33,7 +26,7 @@ export class IntervalSource extends Source { | |||
| max: 1, | |||
| format: (value: number) => { | |||
| if (value < 0) { | |||
| return Math.round(value * 100) + "L"; | |||
| return Math.round(-value * 100) + "L"; | |||
| } else if (value > 0) { | |||
| return Math.round(value * 100) + "R"; | |||
| } else { | |||
| @@ -1,5 +1,5 @@ | |||
| import { Source } from "./Source"; | |||
| import { context, exposedNumber } from "../audio"; | |||
| import { context } from "../audio"; | |||
| export class LoopingSource extends Source { | |||
| kind = "Looping"; | |||
| @@ -7,14 +7,6 @@ export class LoopingSource extends Source { | |||
| private started = false; | |||
| private running = false; | |||
| @exposedNumber({ | |||
| name: "Pitch", | |||
| min: 0.25, | |||
| max: 4, | |||
| format: (value: number) => Math.round(value * 100) + "%", | |||
| }) | |||
| public pitch = 1; | |||
| constructor(name: string) { | |||
| super(name); | |||
| } | |||
| @@ -14,6 +14,8 @@ export function makeGlorps(): Source { | |||
| source.loadSound("bowels-churn-safe"); | |||
| source.loadSound("bowels-churn-danger"); | |||
| console.log(source); | |||
| return source; | |||
| } | |||
| @@ -28,6 +28,16 @@ export abstract class Source extends Node { | |||
| }) | |||
| 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) { | |||
| super(name); | |||
| this.gain = context.createGain(); | |||