| @@ -48,18 +48,24 @@ export abstract class Node { | |||||
| constructor(public name: string) {} | constructor(public name: string) {} | ||||
| } | } | ||||
| export type NumberMetadata = { | |||||
| export type PropMetadata = { | |||||
| name: string; | name: string; | ||||
| }; | |||||
| export type NumberMetadata = PropMetadata & { | |||||
| min: number; | min: number; | ||||
| max: number; | max: number; | ||||
| format?: (value: number) => string; | format?: (value: number) => string; | ||||
| map?: (value: number) => number; | |||||
| unmap?: (value: number) => number; | |||||
| }; | }; | ||||
| export type RangeMetadata = { | |||||
| name: string; | |||||
| export type RangeMetadata = PropMetadata & { | |||||
| min: number; | min: number; | ||||
| max: number; | max: number; | ||||
| format?: (value: number) => string; | format?: (value: number) => string; | ||||
| map?: (value: number) => number; | |||||
| unmap?: (value: number) => number; | |||||
| }; | }; | ||||
| export const exposedMetadataNumber = Symbol("exposedNumber"); | 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> | <template> | ||||
| <div class="node-props"> | <div class="node-props"> | ||||
| <div | |||||
| class="node-prop" | |||||
| <node-number-prop | |||||
| v-for="(metadata, index) in numberProps" | v-for="(metadata, index) in numberProps" | ||||
| :node="node" | |||||
| :key="index" | :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> | </div> | ||||
| </template> | </template> | ||||
| @@ -39,6 +25,8 @@ import { | |||||
| import { Options, Vue } from "vue-class-component"; | import { Options, Vue } from "vue-class-component"; | ||||
| import Slider from "@vueform/slider"; | import Slider from "@vueform/slider"; | ||||
| import { Node } from "@/audio"; | import { Node } from "@/audio"; | ||||
| import NodeNumberProp from "@/components/NodeNumberProp.vue"; | |||||
| import NodeRangeProp from "@/components/NodeRangeProp.vue"; | |||||
| @Options({ | @Options({ | ||||
| props: { | props: { | ||||
| @@ -46,6 +34,8 @@ import { Node } from "@/audio"; | |||||
| }, | }, | ||||
| components: { | components: { | ||||
| Slider, | Slider, | ||||
| NodeNumberProp, | |||||
| NodeRangeProp, | |||||
| }, | }, | ||||
| }) | }) | ||||
| export default class NodeProps extends Vue { | 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", | name: "Cutoff Frequency", | ||||
| min: 10, | min: 10, | ||||
| max: 10000, | 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; | public cutoff = 500; | ||||
| @@ -8,7 +8,12 @@ export class BiquadFilter extends Filter { | |||||
| name: "Cutoff Frequency", | name: "Cutoff Frequency", | ||||
| min: 10, | min: 10, | ||||
| max: 10000, | 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; | public cutoff = 1000; | ||||
| @@ -1,29 +1,22 @@ | |||||
| import { Source } from "./Source"; | import { Source } from "./Source"; | ||||
| import { exposedNumber, context, exposedRange } from "../audio"; | |||||
| import { context, exposedRange } from "../audio"; | |||||
| export class IntervalSource extends Source { | export class IntervalSource extends Source { | ||||
| kind = "Interval"; | kind = "Interval"; | ||||
| @exposedNumber({ | |||||
| name: "Pitch", | |||||
| min: 0.25, | |||||
| max: 4, | |||||
| format: (value: number) => Math.round(value * 100) + "%", | |||||
| }) | |||||
| public pitch = 1; | |||||
| // | |||||
| @exposedNumber({ | |||||
| @exposedRange({ | |||||
| name: "Interval", | name: "Interval", | ||||
| min: 0.25, | min: 0.25, | ||||
| max: 30, | |||||
| max: 300, | |||||
| format: (value: number) => { | format: (value: number) => { | ||||
| return ( | return ( | ||||
| value.toLocaleString(undefined, { | |||||
| Math.pow(2, value).toLocaleString(undefined, { | |||||
| maximumFractionDigits: 2, | maximumFractionDigits: 2, | ||||
| }) + "s" | }) + "s" | ||||
| ); | ); | ||||
| }, | }, | ||||
| map: (value: number) => Math.log(value) / Math.log(2), | |||||
| unmap: (value: number) => Math.pow(2, value), | |||||
| }) | }) | ||||
| public interval: [number, number] = [4, 6]; | public interval: [number, number] = [4, 6]; | ||||
| @@ -33,7 +26,7 @@ export class IntervalSource extends Source { | |||||
| max: 1, | max: 1, | ||||
| format: (value: number) => { | format: (value: number) => { | ||||
| if (value < 0) { | if (value < 0) { | ||||
| return Math.round(value * 100) + "L"; | |||||
| return Math.round(-value * 100) + "L"; | |||||
| } else if (value > 0) { | } else if (value > 0) { | ||||
| return Math.round(value * 100) + "R"; | return Math.round(value * 100) + "R"; | ||||
| } else { | } else { | ||||
| @@ -1,5 +1,5 @@ | |||||
| import { Source } from "./Source"; | import { Source } from "./Source"; | ||||
| import { context, exposedNumber } from "../audio"; | |||||
| import { context } from "../audio"; | |||||
| export class LoopingSource extends Source { | export class LoopingSource extends Source { | ||||
| kind = "Looping"; | kind = "Looping"; | ||||
| @@ -7,14 +7,6 @@ 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(value * 100) + "%", | |||||
| }) | |||||
| public pitch = 1; | |||||
| constructor(name: string) { | constructor(name: string) { | ||||
| super(name); | super(name); | ||||
| } | } | ||||
| @@ -14,6 +14,8 @@ export function makeGlorps(): Source { | |||||
| source.loadSound("bowels-churn-safe"); | source.loadSound("bowels-churn-safe"); | ||||
| source.loadSound("bowels-churn-danger"); | source.loadSound("bowels-churn-danger"); | ||||
| console.log(source); | |||||
| return source; | return source; | ||||
| } | } | ||||
| @@ -28,6 +28,16 @@ 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(); | ||||