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
12 changes: 12 additions & 0 deletions projects/web-synth-9x1a/JOURNAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,15 @@
- [ ] Add a "Reset to Default" button to Distortion Node
- [ ] Add a "Reset to Default" button to Filter Node
- [ ] Add a "Reset to Default" button to Reverb Node

## Expansion Phase 12 (New Improvements)
- [x] Add a Drive/Saturation parameter to the Filter Node
- [x] Add Bypass switch to Chorus Node
- [x] Add Bypass switch to Bitcrusher Node
- [x] Add Bypass switch to Compressor Node
- [x] Add "Reset to Default" button to Chorus Node
- [x] Add "Reset to Default" button to Compressor Node
- [x] Add "Reset to Default" button to Bitcrusher Node
- [x] Add "Randomize Parameters" button to Chorus Node
- [x] Add "Randomize Parameters" button to Tremolo Node
- [x] Add Bypass switch to Tremolo Node
194 changes: 189 additions & 5 deletions projects/web-synth-9x1a/src/audio/nodes/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,15 @@ export class FilterWrapper {
public dryGain: GainNode;
public wetGain: GainNode;
public outputNode: GainNode;
public driveNode: WaveShaperNode;

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

this.inputNode = ctx.createGain();
this.node = ctx.createBiquadFilter();
this.node.type = 'lowpass';
this.driveNode = ctx.createWaveShaper();
this.node.frequency.value = 1000;
this.node.Q.value = 1;
this.dryGain = ctx.createGain();
Expand All @@ -64,7 +66,8 @@ export class FilterWrapper {

this.inputNode.connect(this.node);
this.inputNode.connect(this.dryGain);
this.node.connect(this.wetGain);
this.node.connect(this.driveNode);
this.driveNode.connect(this.wetGain);
this.dryGain.connect(this.outputNode);
this.wetGain.connect(this.outputNode);

Expand All @@ -85,6 +88,19 @@ export class FilterWrapper {
audioCore.registerParam(`${id}.Q`, this.node.Q);
}

public setDrive(amount: number) {
const ctx = audioCore.getContext();
const k = typeof amount === 'number' ? amount : 50;
const n_samples = ctx.sampleRate;
const curve = new Float32Array(n_samples);
const deg = Math.PI / 180;
for (let i = 0; i < n_samples; ++i) {
const x = i * 2 / n_samples - 1;
curve[i] = (3 + k) * x * 20 * deg / (Math.PI + k * Math.abs(x));
}
this.driveNode.curve = curve;
}

public setType(type: BiquadFilterType) {
this.node.type = type;
}
Expand All @@ -105,6 +121,7 @@ export class FilterWrapper {
public destroy(id: string) {
this.inputNode.disconnect();
this.node.disconnect();
this.driveNode.disconnect();
this.dryGain.disconnect();
this.wetGain.disconnect();
this.outputNode.disconnect();
Expand Down Expand Up @@ -454,18 +471,49 @@ export class DistortionWrapper {
}

export class CompressorWrapper {
public inputNode: GainNode;
public node: DynamicsCompressorNode;
public dryGain: GainNode;
public wetGain: GainNode;
public outputNode: GainNode;
public isBypassed: boolean = false;

constructor(id: string) {
const ctx = audioCore.getContext();
this.inputNode = ctx.createGain();
this.node = ctx.createDynamicsCompressor();
this.dryGain = ctx.createGain();
this.wetGain = ctx.createGain();
this.outputNode = ctx.createGain();

this.dryGain.gain.value = 0;
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.node.threshold.value = -24;
this.node.knee.value = 30;
this.node.ratio.value = 12;
this.node.attack.value = 0.003;
this.node.release.value = 0.25;

audioCore.registerNode(id, this.node);
audioCore.registerNode(id, this.inputNode);
audioCore.registerParam(`${id}.threshold`, this.node.threshold);
audioCore.registerParam(`${id}.knee`, this.node.knee);
audioCore.registerParam(`${id}.ratio`, this.node.ratio);
Expand Down Expand Up @@ -493,8 +541,29 @@ export class CompressorWrapper {
this.node.release.setValueAtTime(value, 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 {
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);
audioCore.unregisterParam(`${id}.threshold`);
audioCore.unregisterParam(`${id}.knee`);
Expand All @@ -512,6 +581,7 @@ export class ChorusWrapper {
public dryNode: GainNode;
public wetNode: GainNode;
public outputNode: GainNode;
public isBypassed: boolean = false;

constructor(id: string) {
const ctx = audioCore.getContext();
Expand Down Expand Up @@ -569,7 +639,19 @@ export class ChorusWrapper {
this.lfoGain.gain.setValueAtTime(depth, audioCore.getContext().currentTime);
}

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

public setMix(mix: number) {
if (this.isBypassed) return;
this.dryNode.gain.setValueAtTime(Math.cos(mix * 0.5 * Math.PI), audioCore.getContext().currentTime);
this.wetNode.gain.setValueAtTime(Math.cos((1.0 - mix) * 0.5 * Math.PI), audioCore.getContext().currentTime);
}
Expand All @@ -588,14 +670,61 @@ export class ChorusWrapper {
}

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

constructor(id: string) {
const ctx = audioCore.getContext();
this.inputNode = ctx.createGain();
this.node = ctx.createWaveShaper();
this.dryGain = ctx.createGain();
this.wetGain = ctx.createGain();
this.outputNode = ctx.createGain();

this.dryGain.gain.value = 0;
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.setBitDepth(8);
audioCore.registerNode(id, this.inputNode);
}

audioCore.registerNode(id, this.node);
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 setBitDepth(bits: number) {
Expand All @@ -613,7 +742,11 @@ export class BitcrusherWrapper {
}

public destroy(id: string) {
this.inputNode.disconnect();
this.node.disconnect();
this.dryGain.disconnect();
this.wetGain.disconnect();
this.outputNode.disconnect();
audioCore.unregisterNode(id);
}
}
Expand All @@ -622,24 +755,54 @@ export class TremoloWrapper {
public inputNode: GainNode;
public lfo: OscillatorNode;
public lfoGain: GainNode;
public dryGain: GainNode;
public wetGain: GainNode;
public outputNode: GainNode;
public effectGain: GainNode;
public isBypassed: boolean = false;

constructor(id: string) {
const ctx = audioCore.getContext();
this.inputNode = ctx.createGain();
this.lfo = ctx.createOscillator();
this.lfoGain = ctx.createGain();
this.dryGain = ctx.createGain();
this.wetGain = ctx.createGain();
this.outputNode = ctx.createGain();
this.effectGain = ctx.createGain();

this.inputNode.gain.value = 1.0;
this.lfo.type = 'sine';

this.dryGain.gain.value = 0;
this.wetGain.gain.value = 1;

// Modulation values
this.lfo.frequency.value = 5.0; // rate
this.lfoGain.gain.value = 0.5; // depth

this.lfo.connect(this.lfoGain);

// Modulate the gain of inputNode
this.lfoGain.connect(this.inputNode.gain);
// Modulate the gain of effectGain instead of inputNode
this.lfoGain.connect(this.effectGain.gain);

this.inputNode.connect(this.dryGain);
this.inputNode.connect(this.effectGain);
this.effectGain.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.lfo.start();

Expand All @@ -654,11 +817,32 @@ export class TremoloWrapper {
this.lfoGain.gain.setValueAtTime(depth, 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 {
this.dryGain.gain.setValueAtTime(0, audioCore.getContext().currentTime);
this.wetGain.gain.setValueAtTime(1, audioCore.getContext().currentTime);
}
}

public destroy(id: string) {
this.lfo.stop();
this.lfo.disconnect();
this.lfoGain.disconnect();
this.inputNode.disconnect();
this.dryGain.disconnect();
this.wetGain.disconnect();
this.effectGain.disconnect();
this.outputNode.disconnect();
audioCore.unregisterNode(id);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export function BitcrusherNode({ 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-500" /></label>
<label className="text-xs text-gray-300 flex flex-col">
Bits: {data.bits || 8}
<input
Expand All @@ -38,6 +39,8 @@ export function BitcrusherNode({ 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>
<button onClick={() => updateNodeData(id, { bits: 8, mix: 1.0, bypass: false })} className="mt-1 bg-gray-700 hover:bg-gray-600 text-xs py-1 rounded text-gray-300 transition-colors">Reset to Default</button>
</div>

<Handle type="target" position={Position.Left} id="in" className="w-3 h-3 bg-orange-500" />
Expand Down
14 changes: 4 additions & 10 deletions projects/web-synth-9x1a/src/components/nodes/ChorusNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export function ChorusNode({ 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-teal-500" /></label>
<label className="text-xs text-gray-300 flex flex-col">
Rate: {data.rate} Hz
<input
Expand All @@ -50,16 +51,9 @@ export function ChorusNode({ id, data }: { id: string, data: Record<string, any>
/>
</label>

<label className="text-xs text-gray-300 flex flex-col">
Mix: {data.mix}
<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>
<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, { rate: Math.random() * 10, depth: Math.random() * 0.02, 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>
<button onClick={() => updateNodeData(id, { rate: 1.5, depth: 0.005, mix: 0.5, bypass: false })} className="mt-1 bg-gray-700 hover:bg-gray-600 text-xs py-1 rounded text-gray-300 transition-colors">Reset to Default</button>
</div>

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