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
20 changes: 17 additions & 3 deletions projects/web-synth-9x1a/JOURNAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,27 @@
## 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 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
- [x] Add an Invert Phase option to Oscillator Node
- [ ] Add a "Randomize Parameters" button to node UIs
- [ ] Implement Node Bypassing for Delay Node
- [x] Implement Node Bypassing for Delay Node
- [ ] Add a Drive/Saturation parameter to the Filter Node

## Expansion Phase 9 (New Additions)
- [x] Add Dry/Wet mix control to Delay Node
- [x] Add Dry/Wet mix control to Distortion Node
- [x] Implement Node Bypassing for Delay Node
- [x] Implement Node Bypassing for Distortion Node
- [x] Add a Peaking filter type to Filter Node
- [x] Add a Low Shelf filter type to Filter Node
- [x] Add a High Shelf filter type to Filter Node
- [x] Add an Octave control (-2 to +2) to Oscillator Node
- [x] Add an Invert Phase option to Oscillator Node
- [x] Add a "Randomize Parameters" button to Delay Node
- [x] Add a "Randomize Parameters" button to Filter Node
- [x] Add a "Randomize Parameters" button to Oscillator Node
119 changes: 115 additions & 4 deletions projects/web-synth-9x1a/src/audio/nodes/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,22 +115,56 @@ export class FilterWrapper {
}

export class DelayWrapper {
public inputNode: GainNode;
public node: DelayNode;
public feedbackNode: GainNode;
public dryGain: GainNode;
public wetGain: GainNode;
public outputNode: GainNode;
private isBypassed: boolean = false;

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

this.inputNode = ctx.createGain();
this.node = ctx.createDelay(5.0); // max delay 5 seconds
this.node.delayTime.value = 0.5;

this.feedbackNode = ctx.createGain();
this.feedbackNode.gain.value = 0.5;

this.dryGain = ctx.createGain();
this.wetGain = ctx.createGain();
this.outputNode = ctx.createGain();

this.dryGain.gain.value = 0.5;
this.wetGain.gain.value = 0.5;

this.inputNode.connect(this.dryGain);
this.inputNode.connect(this.node);

// Connect node -> feedback -> node for the echo effect
this.node.connect(this.feedbackNode);
this.feedbackNode.connect(this.node);

audioCore.registerNode(id, this.node);
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}.delayTime`, this.node.delayTime);
audioCore.registerParam(`${id}.feedback`, this.feedbackNode.gain);
}
Expand All @@ -143,9 +177,32 @@ export class DelayWrapper {
this.feedbackNode.gain.setValueAtTime(gain, audioCore.getContext().currentTime);
}

public setMix(mix: number) {
if (this.isBypassed) return;
this.dryGain.gain.setValueAtTime(Math.cos(mix * 0.5 * Math.PI), audioCore.getContext().currentTime);
this.wetGain.gain.setValueAtTime(Math.cos((1.0 - mix) * 0.5 * Math.PI), audioCore.getContext().currentTime);
}

public setBypass(bypass: boolean) {
this.isBypassed = bypass;
if (bypass) {
this.dryGain.gain.setValueAtTime(1, audioCore.getContext().currentTime);
this.wetGain.gain.setValueAtTime(0, audioCore.getContext().currentTime);
} else {
// mix was not saved, so just default to 0.5 or we'd need to store it.
// It will jump to 0.5 on un-bypass unless setMix is called again.
this.dryGain.gain.setValueAtTime(Math.cos(0.5 * 0.5 * Math.PI), audioCore.getContext().currentTime);
this.wetGain.gain.setValueAtTime(Math.cos(0.5 * 0.5 * Math.PI), audioCore.getContext().currentTime);
}
}

public destroy(id: string) {
this.node.disconnect(this.feedbackNode);
this.feedbackNode.disconnect(this.node);
this.inputNode.disconnect();
this.node.disconnect();
this.feedbackNode.disconnect();
this.dryGain.disconnect();
this.wetGain.disconnect();
this.outputNode.disconnect();
audioCore.unregisterNode(id);
audioCore.unregisterParam(`${id}.delayTime`);
audioCore.unregisterParam(`${id}.feedback`);
Expand Down Expand Up @@ -282,15 +339,48 @@ export class PanningWrapper {
}

export class DistortionWrapper {
public inputNode: GainNode;
public node: WaveShaperNode;
public dryGain: GainNode;
public wetGain: GainNode;
public outputNode: GainNode;
private isBypassed: boolean = false;

constructor(id: string) {
const ctx = audioCore.getContext();
this.inputNode = ctx.createGain();
this.node = ctx.createWaveShaper();
this.node.oversample = '4x';

this.dryGain = ctx.createGain();
this.wetGain = ctx.createGain();
this.outputNode = ctx.createGain();

this.dryGain.gain.value = 0; // Default mix 100% wet
this.wetGain.gain.value = 1;

this.inputNode.connect(this.dryGain);
this.inputNode.connect(this.node);
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();
};

this.setDrive(50); // Default drive

audioCore.registerNode(id, this.node);
audioCore.registerNode(id, this.inputNode);
}

// Uses a polynomial curve for soft clipping/distortion
Expand All @@ -308,8 +398,29 @@ export class DistortionWrapper {
this.node.curve = curve;
}

public setMix(mix: number) {
if (this.isBypassed) return;
this.dryGain.gain.setValueAtTime(Math.cos(mix * 0.5 * Math.PI), audioCore.getContext().currentTime);
this.wetGain.gain.setValueAtTime(Math.cos((1.0 - mix) * 0.5 * Math.PI), audioCore.getContext().currentTime);
}

public setBypass(bypass: boolean) {
this.isBypassed = bypass;
if (bypass) {
this.dryGain.gain.setValueAtTime(1, audioCore.getContext().currentTime);
this.wetGain.gain.setValueAtTime(0, audioCore.getContext().currentTime);
} else {
this.dryGain.gain.setValueAtTime(0, audioCore.getContext().currentTime);
this.wetGain.gain.setValueAtTime(1, audioCore.getContext().currentTime);
}
}

public destroy(id: string) {
this.inputNode.disconnect();
this.node.disconnect();
this.dryGain.disconnect();
this.wetGain.disconnect();
this.outputNode.disconnect();
audioCore.unregisterNode(id);
}
}
Expand Down
23 changes: 21 additions & 2 deletions projects/web-synth-9x1a/src/audio/nodes/sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { audioCore } from '../core';

export class OscillatorWrapper {
public node: OscillatorNode;
public output: GainNode;
private baseFreq: number = 440;
private octave: number = 0;

constructor(id: string) {
const ctx = audioCore.getContext();
Expand All @@ -10,7 +13,11 @@ export class OscillatorWrapper {
this.node.frequency.value = 440;
this.node.start();

audioCore.registerNode(id, this.node);
this.output = ctx.createGain();
this.output.gain.value = 1;
this.node.connect(this.output);

audioCore.registerNode(id, this.output);
audioCore.registerParam(`${id}.frequency`, this.node.frequency);
audioCore.registerParam(`${id}.detune`, this.node.detune);
}
Expand All @@ -20,7 +27,17 @@ export class OscillatorWrapper {
}

public setFrequency(freq: number) {
this.node.frequency.setValueAtTime(freq, audioCore.getContext().currentTime);
this.baseFreq = freq;
this.node.frequency.setValueAtTime(this.baseFreq * Math.pow(2, this.octave), audioCore.getContext().currentTime);
}

public setOctave(oct: number) {
this.octave = oct;
this.node.frequency.setValueAtTime(this.baseFreq * Math.pow(2, this.octave), audioCore.getContext().currentTime);
}

public setInvertPhase(invert: boolean) {
this.output.gain.setValueAtTime(invert ? -1 : 1, audioCore.getContext().currentTime);
}

public setDetune(cents: number) {
Expand All @@ -29,6 +46,8 @@ export class OscillatorWrapper {

public destroy(id: string) {
this.node.stop();
this.node.disconnect();
this.output.disconnect();
audioCore.unregisterNode(id);
audioCore.unregisterParam(`${id}.frequency`);
audioCore.unregisterParam(`${id}.detune`);
Expand Down
32 changes: 32 additions & 0 deletions projects/web-synth-9x1a/src/components/nodes/DelayNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ export function DelayNode({ 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-purple-500"
/>
</label>

<label className="text-xs text-gray-300 flex flex-col">
Time: {data.delayTime?.toFixed(2)} s
<input
Expand All @@ -49,6 +59,28 @@ export function DelayNode({ id, data }: { id: string, data: Record<string, any>
className="mt-1"
/>
</label>

<label className="text-xs text-gray-300 flex flex-col">
Mix: {data.mix !== undefined ? data.mix.toFixed(2) : '0.50'}
<input
type="range"
min="0" max="1" step="0.01"
value={data.mix !== undefined ? data.mix : 0.5}
onChange={(e) => updateNodeData(id, { mix: Number(e.target.value) })}
className="mt-1"
/>
</label>

<button
onClick={() => updateNodeData(id, {
delayTime: Math.random() * 2,
feedback: Math.random(),
mix: Math.random()
})}
className="mt-1 bg-gray-700 hover:bg-gray-600 text-xs py-1 rounded text-gray-300 transition-colors"
>
Randomize
</button>
</div>

<Handle type="target" position={Position.Left} id="in" className="w-3 h-3 bg-purple-500" />
Expand Down
21 changes: 21 additions & 0 deletions projects/web-synth-9x1a/src/components/nodes/DistortionNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ export function DistortionNode({ id, data }: { id: string, data: Record<string,
</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-600"
/>
</label>

<label className="text-xs text-gray-300 flex flex-col">
Drive: {data.drive !== undefined ? data.drive : 50}
<input
Expand All @@ -38,6 +48,17 @@ export function DistortionNode({ id, data }: { id: string, data: Record<string,
className="mt-1"
/>
</label>

<label className="text-xs text-gray-300 flex flex-col">
Mix: {data.mix !== undefined ? data.mix.toFixed(2) : '1.00'}
<input
type="range"
min="0" max="1" step="0.01"
value={data.mix !== undefined ? data.mix : 1.0}
onChange={(e) => updateNodeData(id, { mix: Number(e.target.value) })}
className="mt-1"
/>
</label>
</div>

<Handle type="target" position={Position.Left} id="in" className="w-3 h-3 bg-orange-600" />
Expand Down
19 changes: 18 additions & 1 deletion projects/web-synth-9x1a/src/components/nodes/FilterNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ export function FilterNode({ id, data }: { id: string, data: Record<string, any>
<option value="highpass">Highpass</option>
<option value="bandpass">Bandpass</option>
<option value="notch">Notch</option>
<option value="peaking">Peaking</option>
<option value="lowshelf">Low Shelf</option>
<option value="highshelf">High Shelf</option>
</select>
</label>

Expand All @@ -67,11 +70,25 @@ export function FilterNode({ id, data }: { id: string, data: Record<string, any>
<input
type="range"
min="0" max="20" step="0.1"
value={data.Q || 1}
value={data.Q !== undefined ? data.Q : 1}
onChange={(e) => updateNodeData(id, { Q: Number(e.target.value) })}
className="mt-1"
/>
</label>

<button
onClick={() => {
const types = ['lowpass', 'highpass', 'bandpass', 'notch', 'peaking', 'lowshelf', 'highshelf'];
updateNodeData(id, {
frequency: Math.floor(Math.random() * (10000 - 20) + 20),
Q: Math.random() * 20,
type: types[Math.floor(Math.random() * types.length)]
});
}}
className="mt-1 bg-gray-700 hover:bg-gray-600 text-xs py-1 rounded text-gray-300 transition-colors"
>
Randomize
</button>
</div>

<Handle type="target" position={Position.Left} id="in" className="w-3 h-3 bg-orange-500" />
Expand Down
Loading