Skip to content

Commit

Permalink
first working node effect
Browse files Browse the repository at this point in the history
  • Loading branch information
Geoxor committed Oct 24, 2022
1 parent bbdb783 commit 7cd692c
Show file tree
Hide file tree
Showing 12 changed files with 225 additions and 81 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "amethyst",
"author": "Geoxor <[email protected]>",
"productName": "Amethyst",
"version": "1.7.6",
"version": "1.7.7",
"main": "./release/dist/main/main.js",
"licenses": [
{
Expand Down
2 changes: 1 addition & 1 deletion src/main/mainWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ export class MainWindow {
return;

return (
await sharp(cover).resize(resizeTo, resizeTo).png().toBuffer()
await sharp(cover).resize(resizeTo, resizeTo).webp().toBuffer()
).toString("base64");
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/renderer/components/DbMeter.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { usePlayer } from "../amethyst";
import { onMounted, onUnmounted, ref, computed } from "vue";
const props = defineProps<{ node: MediaElementAudioSourceNode }>();
const props = defineProps<{ node: AudioNode }>();
const FLOOR = -60;
const RANGE = 30;
const FFT_SIZE = 2048 * 2;
Expand Down Expand Up @@ -76,7 +76,7 @@ onMounted(() => {
const computedWidth = (value: number): number => {
const width = (1 + value / RANGE) * 90;
// this fixes the animation jump from 0 to the first value
return Math.max(0.01, width);
return Math.min(100, Math.max(0.01, width));
};
onUnmounted(() => shouldStopRendering = true);
Expand Down
50 changes: 5 additions & 45 deletions src/renderer/components/NodeEditor.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { Position, VueFlow } from '@vue-flow/core'
import { computed, onMounted, ref } from 'vue';
import { VueFlow } from '@vue-flow/core'
import { Background, BackgroundVariant } from '@vue-flow/additional-components'
import { getThemeColorHex } from '../logic/color';
import CustomNode from '../components/nodes/CustomNode.vue';
import StepIntoIcon from '../icons/nodes/StepIntoIcon.vue';
import StepOutIcon from '../icons/nodes/StepOutIcon.vue';
import WaveIcon from '../icons/nodes/WaveIcon.vue';
import Spectrum from './Spectrum/Spectrum.vue';
import { usePlayer, useState } from '../amethyst';
import MagnetIcon from '../icons/MagnetIcon.vue';
import SquareButton from './SquareButton.vue';
Expand All @@ -20,33 +15,8 @@ onMounted(() => {
const player = usePlayer();
const state = useState();
const elements = ref([
{
id: 'input',
type: 'custom-input',
position: { x: 0, y: 250 },
sourcePosition: Position.Right,
},
const elements = computed(() => [...player.nodeManager.getNodeProperties(), ...player.nodeManager.getNodeConnections()]);
{
id: 'spectrum',
type: 'custom-spectrum',
position: { x: 300, y: 350 },
targetPosition: Position.Left,
},
{
id: 'output',
type: 'custom-output',
position: { x: 700, y: 250 },
targetPosition: Position.Left,
},
// Edges
// Most basic edge, only consists of an id, source-id and target-id
{ id: 'input-spectrum', source: 'input', target: 'spectrum', animated: true },
{ id: 'spectrum-output', source: 'spectrum', target: 'output', animated: true },
])
</script>

<template>
Expand All @@ -59,19 +29,9 @@ const elements = ref([
v-model="elements" :connection-line-style="{ stroke: getThemeColorHex('--primary-800') }" :fit-view-on-init="true"
:default-edge-options="{ type: 'smoothstep' }">
<Background :size="0.5" :variant="BackgroundVariant.Dots" :pattern-color="getThemeColorHex('--surface-500')" />
<template #node-custom-input>
<CustomNode title="Input" description="From Amethyst" :icon="StepIntoIcon" />
</template>

<template #node-custom-output>
<CustomNode title="Output" description="To Speakers" :icon="StepOutIcon" />
</template>

<template #node-custom-spectrum>
<CustomNode title="Spectrum" :icon="WaveIcon">
<spectrum v-if="player.state.source" :key="player.state.currentlyPlayingFilePath"
:node="player.state.source" />
</CustomNode>
<template v-for="node of player.nodeManager.nodes" :key="node.properties.id" v-slot:[node.getSlotName()]>
<component :is="node.component" :node="node" />
</template>

</VueFlow>
Expand Down
35 changes: 22 additions & 13 deletions src/renderer/components/nodes/CustomNode.vue
Original file line number Diff line number Diff line change
@@ -1,30 +1,39 @@
<template>
<div :class="disabled && 'disabled'"
class="rounded-4px hover:border-primary-900 duration-50 flex flex-col gap-2 border-surface-500 bg-surface-800 border-1 p-2">
<div class="flex gap-2 items-center">
<component :is="icon" class="text-green-400" />
<h1 class="text-primary-600 uppercase text-9px flex-1">{{ title }}</h1>
<SquareButton :active="!disabled" @click="disabled = !disabled" />
<div :class="node.isDisabled && 'disabled'"
class="flex h-full gap-2 rounded-4px hover:border-primary-900 duration-50 flex gap-2 border-surface-500 bg-surface-800 border-1 p-2">
<div class="flex flex-col gap-2">
<div class="flex gap-2 items-center">
<component :is="icon" class="text-green-400" />
<h1 class="text-primary-600 uppercase text-9px flex-1">{{ title }}</h1>
<SquareButton :active="!node.isDisabled"
@click.stop="node.isDisabled ? player.nodeManager.enableNode(node) : player.nodeManager.disableNode(node)" />
</div>

<slot />
<h1 v-if="description" class=" text-white font-aseprite text-opacity-30"> {{ description }}</h1>
</div>
<slot />
<h1 v-if="description" class=" text-white font-aseprite text-opacity-30"> {{ description }}</h1>

<div class="flex ">
<db-meter v-if="!meterless" :node="node.node" />
</div>
</div>


<Handle id="a" type="source" :position="Position.Right" />
<Handle id="b" type="target" :position="Position.Left" />
</template>

<script setup lang="ts">
import { Handle, Position } from '@vue-flow/core'
import { ref } from 'vue';
import { usePlayer } from '../../amethyst';
import { AmethystAudioNode } from '../../logic/audio';
import DbMeter from '../DbMeter.vue';
import SquareButton from '../SquareButton.vue';
defineProps<{ title: string, icon: any, description?: string }>();
const disabled = ref(false);
defineProps<{ title: string, icon: any, description?: string, node: AmethystAudioNode<any>, meterless?: boolean }>();
const player = usePlayer();
</script>

<style scoped lang="postcss">
<style lang="postcss">
.disabled {
@apply filter grayscale;
}
Expand Down
25 changes: 25 additions & 0 deletions src/renderer/components/nodes/FilterNode.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<template >
<CustomNode :node="node" title="Single Band Filter" :icon="FilterIcon">
<p class="font-aseprite"><strong class="text-white font-aseprite font-thin text-opacity-50">Gain</strong> {{ gain }}
dB</p>

<Slider @mousedown.stop v-model="gain" min="-18" max="18" @change="props.node.node.gain.value = gain" />

<p class="font-aseprite"><strong class="text-white font-aseprite font-thin text-opacity-50">Frequency</strong> {{
frequency
}}
Hz</p>
<Slider @mousedown.stop v-model="frequency" max="2000" @change="props.node.node.frequency.value = frequency" />
</CustomNode>
</template>

<script setup lang="ts">
import { AmethystEqualizerNode } from '../../logic/audio';
import { ref } from 'vue';
import FilterIcon from '../../icons/nodes/FilterIcon.vue';
import Slider from '../input/Slider.vue';
import CustomNode from './CustomNode.vue';
const props = defineProps<{ node: AmethystEqualizerNode }>();
const gain = ref(props.node.node.gain.value);
const frequency = ref(props.node.node.frequency.value);
</script>
11 changes: 11 additions & 0 deletions src/renderer/components/nodes/InputNode.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<template >
<CustomNode :node="node" title="Input" description="From Amethyst" :icon="StepIntoIcon" />
</template>

<script setup lang="ts">
import { AmethystAudioNode } from 'renderer/logic/audio';
import StepIntoIcon from '../../icons/nodes/StepIntoIcon.vue';
import CustomNode from './CustomNode.vue';
defineProps<{ node: AmethystAudioNode<any> }>();
</script>
11 changes: 11 additions & 0 deletions src/renderer/components/nodes/OutputNode.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<template >
<CustomNode meterless :node="node" title="Output" description="To Speakers" :icon="StepOutIcon"></CustomNode>
</template>

<script setup lang="ts">
import { AmethystAudioNode } from 'renderer/logic/audio';
import StepOutIcon from '../../icons/nodes/StepOutIcon.vue';
import CustomNode from './CustomNode.vue';
defineProps<{ node: AmethystAudioNode<any> }>();
</script>
15 changes: 15 additions & 0 deletions src/renderer/icons/nodes/FilterIcon.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<template>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 172 172"
width="16" height="16">
<g fill="none" fill-rule="nonzero" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter"
stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none"
font-size="none" text-anchor="none" style="mix-blend-mode: normal">
<path d="M0,172v-172h172v172z" fill="none"></path>
<g fill="currentColor">
<path
d="M50.16667,21.5c-3.956,0 -7.16667,3.21067 -7.16667,7.16667c0,3.956 3.21067,7.16667 7.16667,7.16667c3.956,0 7.16667,-3.21067 7.16667,-7.16667c0,-3.956 -3.21067,-7.16667 -7.16667,-7.16667zM121.83333,21.5c-3.956,0 -7.16667,3.21067 -7.16667,7.16667c0,3.956 3.21067,7.16667 7.16667,7.16667c3.956,0 7.16667,-3.21067 7.16667,-7.16667c0,-3.956 -3.21067,-7.16667 -7.16667,-7.16667zM150.36003,28.59668c-1.86189,0.05548 -3.62905,0.83363 -4.92708,2.1696l-23.59961,23.59961l-16.43294,-16.43294c-2.7988,-2.79764 -7.33531,-2.79764 -10.13411,0l-23.59961,23.59961l-16.43294,-16.43294c-2.7988,-2.79764 -7.33532,-2.79764 -10.13411,0l-21.5,21.5c-1.87219,1.79753 -2.62635,4.46674 -1.97162,6.97822c0.65473,2.51148 2.61604,4.47279 5.12752,5.12752c2.51148,0.65473 5.18069,-0.09943 6.97822,-1.97163l16.43294,-16.43294l16.43294,16.43294c2.7988,2.79764 7.33532,2.79764 10.13411,0l23.59961,-23.59961l16.43294,16.43294c2.7988,2.79764 7.33531,2.79764 10.13411,0l28.66667,-28.66667c2.11962,-2.06035 2.75694,-5.21064 1.60486,-7.93287c-1.15207,-2.72224 -3.85719,-4.45797 -6.81189,-4.37084zM50.16667,78.83333c-3.956,0 -7.16667,3.21067 -7.16667,7.16667c0,3.956 3.21067,7.16667 7.16667,7.16667c3.956,0 7.16667,-3.21067 7.16667,-7.16667c0,-3.956 -3.21067,-7.16667 -7.16667,-7.16667zM121.83333,78.83333c-3.956,0 -7.16667,3.21067 -7.16667,7.16667c0,3.956 3.21067,7.16667 7.16667,7.16667c3.956,0 7.16667,-3.21067 7.16667,-7.16667c0,-3.956 -3.21067,-7.16667 -7.16667,-7.16667zM50.16667,107.5c-3.956,0 -7.16667,3.21067 -7.16667,7.16667c0,3.956 3.21067,7.16667 7.16667,7.16667c3.956,0 7.16667,-3.21067 7.16667,-7.16667c0,-3.956 -3.21067,-7.16667 -7.16667,-7.16667zM121.83333,107.5c-3.956,0 -7.16667,3.21067 -7.16667,7.16667c0,3.956 3.21067,7.16667 7.16667,7.16667c3.956,0 7.16667,-3.21067 7.16667,-7.16667c0,-3.956 -3.21067,-7.16667 -7.16667,-7.16667zM28.66667,136.16667c-2.58456,-0.03655 -4.98858,1.32136 -6.29153,3.55376c-1.30295,2.2324 -1.30295,4.99342 0,7.22582c1.30295,2.2324 3.70697,3.59031 6.29153,3.55376h114.66667c2.58456,0.03655 4.98858,-1.32136 6.29153,-3.55376c1.30295,-2.2324 1.30295,-4.99342 0,-7.22582c-1.30295,-2.2324 -3.70697,-3.59031 -6.29153,-3.55376z">
</path>
</g>
</g>
</svg>
</template>
129 changes: 121 additions & 8 deletions src/renderer/logic/audio.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,128 @@
export class AmethystEffect {
constructor(public context: AudioContext) {
import { Position } from "@vue-flow/core";
import { loadFolder } from "main/handles";
import { DefineComponent, Ref, ref } from "vue";
import InputNode from "../components/nodes/InputNode.vue"
import OutputNode from "../components/nodes/OutputNode.vue"
import FilterNode from "../components/nodes/FilterNode.vue"

export interface IAmethystNodeProperties {
id: string,
type: `custom-${string}`,
position: { x: number, y: number },
sourcePosition: Position,
};

export interface IAmethystNodeConnection {
id: `${string}-${string}`,
source: string,
target: string,
animated: true,
};

export class AmethystAudioNodeManager {
public nodes: AmethystAudioNode<AudioNode>[] = ref([]).value;

public constructor(public input: AudioNode, public context: AudioContext) {
const output = this.context.destination;

this.nodes.push(new AmethystAudioNode(input, "input", InputNode, { x: -50, y: 0 }, false))
this.nodes.push(new AmethystEqualizerNode(this.context, "filter", { x: 100, y: 0 }))
this.nodes.push(new AmethystAudioNode(output, "output", OutputNode, { x: 350, y: 0 }, false))

this.connectNodes();
}

private connectNodes() {
const enabledNodes = this.nodes.filter(n => !n.isDisabled);

for (let i = enabledNodes.length - 1; i >= 0; i--) {
const previousNode = enabledNodes[i - 1];
const node = enabledNodes[i];

previousNode && previousNode.connectTo(node);
}
}

private reconnectNodes() {
this.nodes.forEach(node => node.disconnect());
this.connectNodes();
}

public removeNode(id: string) {
const target = this.nodes[this.nodes.findIndex(node => node.properties.id === id)];
if (!target.isRemovable) return;
target.disconnect();
delete this.nodes[this.nodes.findIndex(node => node.properties.id === id)];
this.nodes = this.nodes.filter(n => !!n);
this.connectNodes();
}

public addNode<T extends AudioNode>(node: AmethystAudioNode<T>) {
this.nodes.splice(this.nodes.length - 2, 0, node);
this.connectNodes();
}

public disableNode<T extends AudioNode>(node: AmethystAudioNode<T>) {
node.isDisabled = true;
this.reconnectNodes();
}

public enableNode<T extends AudioNode>(node: AmethystAudioNode<T>) {
node.isDisabled = false;
this.reconnectNodes();
}

public getNodeProperties() {
return this.nodes.map(node => node.properties);
}

public getNodeComponents() {
return this.nodes.map(node => node.component);
}

public getNodeConnections() {
return this.nodes.map(node => node.connection).filter(n => !!n) as IAmethystNodeConnection[];
}
}

export class Filter extends AmethystEffect {
public node: BiquadFilterNode;
export class AmethystAudioNode<T extends AudioNode> {
public properties: IAmethystNodeProperties;
public connection: IAmethystNodeConnection | undefined;
public isDisabled: boolean = false;
private connectedTo: AmethystAudioNode<T> | undefined;

public constructor(public node: T, name: string, public component: DefineComponent<{}, {}, any>, position: IAmethystNodeProperties["position"], public isRemovable: boolean = true) {
this.properties = {
id: name,
type: `custom-${name}`,
position,
sourcePosition: Position.Right,
}
}

constructor(public context: AudioContext) {
super(context);
this.node = this.context.createBiquadFilter();
public getSlotName() {
return `node-${this.properties.type}`;
}

}
public connectTo(target: AmethystAudioNode<T>) {
this.connectedTo = target;
this.connection = { id: `edge-${this.properties.id}-${target.properties.id}`, source: this.properties.id, target: target.properties.id, animated: true }
this.node.connect(target.node);
}

public disconnect() {
this.node.disconnect();
delete this.connection;
}
}

export class AmethystEqualizerNode extends AmethystAudioNode<BiquadFilterNode> {
public constructor(context: AudioContext, name: string, position: IAmethystNodeProperties["position"]) {
const filter = context.createBiquadFilter();
filter.type = "lowshelf";
filter.frequency.value = 100;
filter.gain.value = 0;

super(filter, name, FilterNode, position);
}
}
Loading

0 comments on commit 7cd692c

Please sign in to comment.