Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions projects/web-synth-9x1a/JOURNAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,17 @@
- [ ] Add Volume meter to Output Node
- [ ] Implement node collapsing/folding
- [x] Add a "Reset to Default" button on nodes

## Expansion Phase 8 (New Improvements)
- [x] Fix Reverb Node Mix control wiring in store
- [ ] Fix AudioCore to support separate input and output nodes for complex effects
- [ ] Add Dry/Wet mix control to Delay Node
- [x] Add Phase Inversion toggle to Gain Node
- [x] Implement Node Bypassing for Filter Node
- [ ] Add a Clipping Indicator to Output Node
- [x] Add a DC Offset Node
- [ ] Implement auto-pan LFO sync in Panning Node
- [ ] Add an Invert Phase option to Oscillator Node
- [ ] Add a "Randomize Parameters" button to node UIs
- [ ] Implement Node Bypassing for Delay Node
- [ ] Add a Drive/Saturation parameter to the Filter Node
5 changes: 5 additions & 0 deletions projects/web-synth-9x1a/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { ReverbNode } from './components/nodes/ReverbNode';
import { AnalyserNode } from './components/nodes/AnalyserNode';
import { OutputNode } from './components/nodes/OutputNode';
import { AdsrNode } from './components/nodes/AdsrNode';
import { DcOffsetNode } from './components/nodes/DcOffsetNode';
import { ChorusNode } from './components/nodes/ChorusNode';
import { Settings, Waves, Sliders, AudioWaveform, Activity, MonitorPlay, X } from 'lucide-react';
import { useState } from 'react';
Expand All @@ -37,6 +38,7 @@ const nodeTypes = {
reverbNode: ReverbNode,
analyserNode: AnalyserNode,
adsrNode: AdsrNode,
dcOffsetNode: DcOffsetNode,
chorusNode: ChorusNode,
outputNode: OutputNode,
};
Expand Down Expand Up @@ -71,6 +73,9 @@ export default function App() {
<Waves size={18} className="mb-1 text-gray-400" />
Noise
</button>
<button onClick={() => handleAddNode('dcOffsetNode')} className="flex flex-col items-center justify-center p-3 bg-gray-700 hover:bg-gray-600 rounded-lg text-xs transition-colors">
DC Offset
</button>
<button onClick={() => handleAddNode('lfoNode')} className="flex flex-col items-center justify-center p-3 bg-gray-700 hover:bg-gray-600 rounded-lg text-xs transition-colors col-span-2">
<Activity size={18} className="mb-1 text-blue-400" />
LFO
Expand Down
62 changes: 53 additions & 9 deletions projects/web-synth-9x1a/src/audio/nodes/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export class GainWrapper {
public node: GainNode;
private currentGain: number = 0.5;
private isMuted: boolean = false;
private invertPhase: boolean = false;

constructor(id: string) {
const ctx = audioCore.getContext();
Expand All @@ -14,20 +15,24 @@ export class GainWrapper {
audioCore.registerParam(`${id}.gain`, this.node.gain);
}

public setInvertPhase(invert: boolean) {
this.invertPhase = invert;
this.applyGain();
}

public setGain(value: number) {
this.currentGain = value;
if (!this.isMuted) {
this.node.gain.setValueAtTime(value, audioCore.getContext().currentTime);
}
this.applyGain();
}

public setMute(mute: boolean) {
this.isMuted = mute;
if (this.isMuted) {
this.node.gain.setValueAtTime(0, audioCore.getContext().currentTime);
} else {
this.node.gain.setValueAtTime(this.currentGain, audioCore.getContext().currentTime);
}
this.applyGain();
}

private applyGain() {
const val = this.isMuted ? 0 : (this.invertPhase ? -this.currentGain : this.currentGain);
this.node.gain.setValueAtTime(val, audioCore.getContext().currentTime);
}

public destroy(id: string) {
Expand All @@ -37,16 +42,45 @@ export class GainWrapper {
}

export class FilterWrapper {
public inputNode: GainNode;
public node: BiquadFilterNode;
public dryGain: GainNode;
public wetGain: GainNode;
public outputNode: GainNode;

constructor(id: string) {
const ctx = audioCore.getContext();

this.inputNode = ctx.createGain();
this.node = ctx.createBiquadFilter();
this.node.type = 'lowpass';
this.node.frequency.value = 1000;
this.node.Q.value = 1;
this.dryGain = ctx.createGain();
this.dryGain.gain.value = 0;
this.wetGain = ctx.createGain();
this.wetGain.gain.value = 1;
this.outputNode = ctx.createGain();

audioCore.registerNode(id, this.node);
this.inputNode.connect(this.node);
this.inputNode.connect(this.dryGain);
this.node.connect(this.wetGain);
this.dryGain.connect(this.outputNode);
this.wetGain.connect(this.outputNode);

(this.inputNode as any).connect = (dest: any, output?: number, input?: number) => {
if (output !== undefined && input !== undefined) return this.outputNode.connect(dest, output, input);
if (output !== undefined) return this.outputNode.connect(dest, output);
return this.outputNode.connect(dest);
};
(this.inputNode as any).disconnect = (dest?: any, output?: number, input?: number) => {
if (dest && output !== undefined && input !== undefined) return this.outputNode.disconnect(dest, output, input);
if (dest && output !== undefined) return this.outputNode.disconnect(dest, output);
if (dest) return this.outputNode.disconnect(dest);
return this.outputNode.disconnect();
};

audioCore.registerNode(id, this.inputNode);
audioCore.registerParam(`${id}.frequency`, this.node.frequency);
audioCore.registerParam(`${id}.Q`, this.node.Q);
}
Expand All @@ -63,7 +97,17 @@ export class FilterWrapper {
this.node.Q.setValueAtTime(q, audioCore.getContext().currentTime);
}

public setBypass(bypass: boolean) {
this.dryGain.gain.value = bypass ? 1 : 0;
this.wetGain.gain.value = bypass ? 0 : 1;
}

public destroy(id: string) {
this.inputNode.disconnect();
this.node.disconnect();
this.dryGain.disconnect();
this.wetGain.disconnect();
this.outputNode.disconnect();
audioCore.unregisterNode(id);
audioCore.unregisterParam(`${id}.frequency`);
audioCore.unregisterParam(`${id}.Q`);
Expand Down
14 changes: 14 additions & 0 deletions projects/web-synth-9x1a/src/audio/nodes/sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,17 @@ export class LfoWrapper {
audioCore.unregisterParam(`${id}.depth`);
}
}

export class DcOffsetWrapper {
public node: ConstantSourceNode;
constructor(id: string) {
const ctx = audioCore.getContext();
this.node = ctx.createConstantSource();
this.node.offset.value = 0;
this.node.start();
audioCore.registerNode(id, this.node);
audioCore.registerParam(`${id}.offset`, this.node.offset);
}
public setOffset(v: number) { this.node.offset.setValueAtTime(v, audioCore.getContext().currentTime); }
public destroy(id: string) { this.node.stop(); audioCore.unregisterNode(id); audioCore.unregisterParam(`${id}.offset`); }
}
38 changes: 38 additions & 0 deletions projects/web-synth-9x1a/src/components/nodes/DcOffsetNode.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { X } from 'lucide-react';
import { Handle, Position } from '@xyflow/react';
import { useStore } from '../../store';

export function DcOffsetNode({ id, data }: { id: string, data: Record<string, any> }) {
const updateNodeData = useStore((state) => state.updateNodeData);
const removeNode = useStore((state) => state.removeNode);

return (
<div style={data.color ? { borderColor: data.color } : {}} className="bg-gray-800 border border-gray-700 rounded-md p-3 min-w-[150px] shadow-lg">
<div className="flex justify-between items-center mb-2 border-b border-gray-600 pb-1">
<div className="flex items-center">
<input
value={data.label || 'DC Offset'}
onChange={(e) => updateNodeData(id, { label: e.target.value })}
className={`bg-transparent outline-none w-24 text-sm font-bold ${data.color ? '' : 'text-gray-400'}`}
style={data.color ? { color: data.color } : {}}
/>
<input
type="color"
value={data.color || '#ffffff'}
onChange={(e) => updateNodeData(id, { color: e.target.value })}
className="w-4 h-4 p-0 border-0 bg-transparent cursor-pointer ml-2 opacity-50 hover:opacity-100"
title="Custom Color"
/>
</div>
<button onClick={() => removeNode(id)} className="text-gray-500 hover:text-red-400"><X size={14} /></button>
</div>
<div className="flex flex-col gap-2">
<label className="text-xs text-gray-300 flex flex-col">
Offset: {data.offset !== undefined ? data.offset.toFixed(2) : '0.00'}
<input type="range" min="-1" max="1" step="0.01" value={data.offset || 0} onChange={(e) => updateNodeData(id, { offset: Number(e.target.value) })} className="mt-1" />
</label>
</div>
<Handle type="source" position={Position.Right} id="out" className="w-3 h-3 bg-gray-500" />
</div>
);
}
9 changes: 9 additions & 0 deletions projects/web-synth-9x1a/src/components/nodes/FilterNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ export function FilterNode({ id, data }: { id: string, data: Record<string, any>
</div>

<div className="flex flex-col gap-2">
<label className="text-xs text-gray-300 flex items-center justify-between mb-1">
Bypass
<input
type="checkbox"
checked={data.bypass || false}
onChange={(e) => updateNodeData(id, { bypass: e.target.checked })}
className="ml-2 accent-orange-500"
/>
</label>
<label className="text-xs text-gray-300 flex flex-col">
Type
<select
Expand Down
9 changes: 9 additions & 0 deletions projects/web-synth-9x1a/src/components/nodes/GainNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ export function GainNode({ id, data }: { id: string, data: Record<string, any> }
className="ml-2 accent-green-500"
/>
</label>
<label className="text-xs text-gray-300 flex items-center justify-between mb-1">
Invert Phase
<input
type="checkbox"
checked={data.invertPhase || false}
onChange={(e) => updateNodeData(id, { invertPhase: e.target.checked })}
className="ml-2 accent-green-500"
/>
</label>
<label className="text-xs text-gray-300 flex flex-col">
Level: {data.gain !== undefined ? data.gain.toFixed(2) : '0.50'}
<input
Expand Down
11 changes: 10 additions & 1 deletion projects/web-synth-9x1a/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
} from '@xyflow/react';

import { audioCore } from './audio/core';
import { OscillatorWrapper, NoiseWrapper, LfoWrapper } from './audio/nodes/sources';
import { OscillatorWrapper, NoiseWrapper, LfoWrapper, DcOffsetWrapper } from './audio/nodes/sources';
import { GainWrapper, FilterWrapper, DelayWrapper, ReverbWrapper, PanningWrapper, DistortionWrapper, CompressorWrapper, ChorusWrapper, BitcrusherWrapper, TremoloWrapper, RingModulatorWrapper } from './audio/nodes/processors';
import { AnalyserWrapper } from './audio/nodes/visualizers';
import { AdsrWrapper } from './audio/nodes/control';
Expand Down Expand Up @@ -119,6 +119,10 @@ export const useStore = create<AppState>((set, get) => ({
case 'noiseNode':
wrapper = new NoiseWrapper(id);
break;
case 'dcOffsetNode':
wrapper = new DcOffsetWrapper(id);
initialData = { offset: 0 };
break;
case 'adsrNode':
wrapper = new AdsrWrapper(id);
initialData = { attack: 0.1, decay: 0.1, sustain: 0.5, release: 0.3 };
Expand Down Expand Up @@ -235,9 +239,12 @@ export const useStore = create<AppState>((set, get) => ({

} else if (wrapper instanceof NoiseWrapper) {
if (data.type !== undefined) wrapper.setType(data.type);
} else if (wrapper instanceof DcOffsetWrapper) {
if (data.offset !== undefined) wrapper.setOffset(data.offset);
} else if (wrapper instanceof GainWrapper) {
if (data.gain !== undefined) wrapper.setGain(data.gain);
if (data.muted !== undefined) wrapper.setMute(data.muted);
if (data.invertPhase !== undefined) wrapper.setInvertPhase(data.invertPhase);
} else if (wrapper instanceof CompressorWrapper) {
if (data.threshold !== undefined) wrapper.setThreshold(data.threshold);
if (data.knee !== undefined) wrapper.setKnee(data.knee);
Expand All @@ -252,6 +259,7 @@ export const useStore = create<AppState>((set, get) => ({
if (data.frequency !== undefined) wrapper.setFrequency(data.frequency);
if (data.Q !== undefined) wrapper.setQ(data.Q);
if (data.type !== undefined) wrapper.setType(data.type);
if (data.bypass !== undefined) wrapper.setBypass(data.bypass);
} else if (wrapper instanceof BitcrusherWrapper) {
if (data.bits !== undefined) wrapper.setBitDepth(data.bits);
} else if (wrapper instanceof TremoloWrapper) {
Expand All @@ -265,6 +273,7 @@ export const useStore = create<AppState>((set, get) => ({
if (data.feedback !== undefined) wrapper.setFeedback(data.feedback);
} else if (wrapper instanceof ReverbWrapper) {

if (data.mix !== undefined) wrapper.setMix(data.mix);
if (data.decay !== undefined) wrapper.setDecay(data.decay);
}
},
Expand Down