From 68a3d2fdd61ba07bc0b465a8e5e2ee60c495d3db Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 01:54:05 +0000 Subject: [PATCH] feat(web-synth): Add new phase 12 improvements - Added bypass controls to Chorus, Bitcrusher, Compressor, and Tremolo nodes. - Added mix controls to Bitcrusher, Compressor, and Tremolo nodes. - Added reset to default and randomize buttons. - Added drive parameter to the Filter node. Co-authored-by: kkd16 <112675076+kkd16@users.noreply.github.com> --- projects/web-synth-9x1a/JOURNAL.md | 12 ++ .../src/audio/nodes/processors.ts | 194 +++++++++++++++++- .../src/components/nodes/BitcrusherNode.tsx | 3 + .../src/components/nodes/ChorusNode.tsx | 14 +- .../src/components/nodes/CompressorNode.tsx | 3 + .../src/components/nodes/FilterNode.tsx | 4 + .../src/components/nodes/TremoloNode.tsx | 4 + projects/web-synth-9x1a/src/store.ts | 12 ++ 8 files changed, 231 insertions(+), 15 deletions(-) diff --git a/projects/web-synth-9x1a/JOURNAL.md b/projects/web-synth-9x1a/JOURNAL.md index ad0016bd..f0ec8a08 100644 --- a/projects/web-synth-9x1a/JOURNAL.md +++ b/projects/web-synth-9x1a/JOURNAL.md @@ -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 diff --git a/projects/web-synth-9x1a/src/audio/nodes/processors.ts b/projects/web-synth-9x1a/src/audio/nodes/processors.ts index 973549d9..be296bfd 100644 --- a/projects/web-synth-9x1a/src/audio/nodes/processors.ts +++ b/projects/web-synth-9x1a/src/audio/nodes/processors.ts @@ -47,6 +47,7 @@ export class FilterWrapper { public dryGain: GainNode; public wetGain: GainNode; public outputNode: GainNode; + public driveNode: WaveShaperNode; constructor(id: string) { const ctx = audioCore.getContext(); @@ -54,6 +55,7 @@ export class FilterWrapper { 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(); @@ -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); @@ -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; } @@ -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(); @@ -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); @@ -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`); @@ -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(); @@ -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); } @@ -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) { @@ -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); } } @@ -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(); @@ -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); } } diff --git a/projects/web-synth-9x1a/src/components/nodes/BitcrusherNode.tsx b/projects/web-synth-9x1a/src/components/nodes/BitcrusherNode.tsx index b1d2e337..bcc26963 100644 --- a/projects/web-synth-9x1a/src/components/nodes/BitcrusherNode.tsx +++ b/projects/web-synth-9x1a/src/components/nodes/BitcrusherNode.tsx @@ -28,6 +28,7 @@ export function BitcrusherNode({ id, data }: { id: string, data: Record
+ + +
diff --git a/projects/web-synth-9x1a/src/components/nodes/ChorusNode.tsx b/projects/web-synth-9x1a/src/components/nodes/ChorusNode.tsx index 55f40814..e8c3be6c 100644 --- a/projects/web-synth-9x1a/src/components/nodes/ChorusNode.tsx +++ b/projects/web-synth-9x1a/src/components/nodes/ChorusNode.tsx @@ -28,6 +28,7 @@ export function ChorusNode({ id, data }: { id: string, data: Record
+ - + + +
diff --git a/projects/web-synth-9x1a/src/components/nodes/CompressorNode.tsx b/projects/web-synth-9x1a/src/components/nodes/CompressorNode.tsx index bacb4d9f..83e3e683 100644 --- a/projects/web-synth-9x1a/src/components/nodes/CompressorNode.tsx +++ b/projects/web-synth-9x1a/src/components/nodes/CompressorNode.tsx @@ -28,6 +28,7 @@ export function CompressorNode({ id, data }: { id: string, data: Record
+ + +
diff --git a/projects/web-synth-9x1a/src/components/nodes/FilterNode.tsx b/projects/web-synth-9x1a/src/components/nodes/FilterNode.tsx index 73a2f44a..f12697cd 100644 --- a/projects/web-synth-9x1a/src/components/nodes/FilterNode.tsx +++ b/projects/web-synth-9x1a/src/components/nodes/FilterNode.tsx @@ -37,6 +37,10 @@ export function FilterNode({ id, data }: { id: string, data: Record className="ml-2 accent-orange-500" /> + + + + diff --git a/projects/web-synth-9x1a/src/store.ts b/projects/web-synth-9x1a/src/store.ts index c24f06f8..ca7b3f04 100644 --- a/projects/web-synth-9x1a/src/store.ts +++ b/projects/web-synth-9x1a/src/store.ts @@ -253,6 +253,8 @@ export const useStore = create((set, get) => ({ if (data.ratio !== undefined) wrapper.setRatio(data.ratio); if (data.attack !== undefined) wrapper.setAttack(data.attack); if (data.release !== undefined) wrapper.setRelease(data.release); + if (data.bypass !== undefined) wrapper.setBypass(data.bypass); + if (data.mix !== undefined) wrapper.setMix(data.mix); } else if (wrapper instanceof DistortionWrapper) { if (data.drive !== undefined) wrapper.setDrive(data.drive); if (data.mix !== undefined) wrapper.setMix(data.mix); @@ -284,14 +286,24 @@ export const useStore = create((set, get) => ({ if (data.Q !== undefined) wrapper.setQ(data.Q); if (data.type !== undefined) wrapper.setType(data.type); if (data.bypass !== undefined) wrapper.setBypass(data.bypass); + if (data.drive !== undefined) wrapper.setDrive(data.drive); } else if (wrapper instanceof BitcrusherWrapper) { if (data.bits !== undefined) wrapper.setBitDepth(data.bits); + if (data.bypass !== undefined) wrapper.setBypass(data.bypass); + if (data.mix !== undefined) wrapper.setMix(data.mix); } else if (wrapper instanceof TremoloWrapper) { if (data.rate !== undefined) wrapper.setRate(data.rate); if (data.depth !== undefined) wrapper.setDepth(data.depth); + if (data.bypass !== undefined) wrapper.setBypass(data.bypass); + if (data.mix !== undefined) wrapper.setMix(data.mix); } else if (wrapper instanceof RingModulatorWrapper) { if (data.frequency !== undefined) wrapper.setFrequency(data.frequency); if (data.type !== undefined) wrapper.setType(data.type); + } else if (wrapper instanceof ChorusWrapper) { + if (data.rate !== undefined) wrapper.setRate(data.rate); + if (data.depth !== undefined) wrapper.setDepth(data.depth); + if (data.mix !== undefined) wrapper.setMix(data.mix); + if (data.bypass !== undefined) wrapper.setBypass(data.bypass); } else if (wrapper instanceof DelayWrapper) { if (data.delayTime !== undefined) wrapper.setDelayTime(data.delayTime); if (data.feedback !== undefined) wrapper.setFeedback(data.feedback);