From 8a25563b3d6f52e93d75467d449b62ac37bdc185 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 13:37:32 +0000
Subject: [PATCH] feat: enhance synth with glide, sub-osc, fine-tune and
randomize
- Added Glide (Portamento) control to Oscillator Node
- Added Sub-Oscillator toggle (mixes in an octave down) to Oscillator Node
- Added Fine Tuning (Cents) control to Oscillator Node
- Added 'Randomize Parameters' button to Bitcrusher Node
- Updated JOURNAL.md with completion status and new expansion ideas
Co-authored-by: kkd16 <112675076+kkd16@users.noreply.github.com>
---
projects/web-synth-9x1a/JOURNAL.md | 13 +++++
.../web-synth-9x1a/src/audio/nodes/sources.ts | 51 ++++++++++++++++---
.../src/components/nodes/BitcrusherNode.tsx | 5 +-
.../src/components/nodes/OscillatorNode.tsx | 40 ++++++++++++++-
projects/web-synth-9x1a/src/store.ts | 3 ++
5 files changed, 102 insertions(+), 10 deletions(-)
diff --git a/projects/web-synth-9x1a/JOURNAL.md b/projects/web-synth-9x1a/JOURNAL.md
index f0ec8a08..507e09dd 100644
--- a/projects/web-synth-9x1a/JOURNAL.md
+++ b/projects/web-synth-9x1a/JOURNAL.md
@@ -170,3 +170,16 @@
- [x] Add "Randomize Parameters" button to Chorus Node
- [x] Add "Randomize Parameters" button to Tremolo Node
- [x] Add Bypass switch to Tremolo Node
+
+
+## Expansion Phase 13 (New Features)
+- [x] Add Glide (Portamento) control to Oscillator Node
+- [ ] Add Symmetry/Pulse Width control to Oscillator Node (for Square/Triangle waves)
+- [x] Add Sub-Oscillator toggle (mix in an octave down) to Oscillator Node
+- [x] Add Fine Tuning (Cents) control to Oscillator Node
+- [ ] Implement Bypass switch for Pitch Shift Node (if it existed, or add Pitch Shift node)
+- [ ] Add Dry/Wet control to Bitcrusher Node
+- [ ] Add Drive control to Gain Node to act as a preamp
+- [ ] Add LFO Destination selection in LFO Node (pitch, filter, amp)
+- [x] Add 'Randomize Parameters' button to Bitcrusher Node
+- [ ] Add a visual meter (VU meter style) to Gain Node
diff --git a/projects/web-synth-9x1a/src/audio/nodes/sources.ts b/projects/web-synth-9x1a/src/audio/nodes/sources.ts
index 33c7b58c..59d3512c 100644
--- a/projects/web-synth-9x1a/src/audio/nodes/sources.ts
+++ b/projects/web-synth-9x1a/src/audio/nodes/sources.ts
@@ -5,6 +5,11 @@ export class OscillatorWrapper {
public output: GainNode;
private baseFreq: number = 440;
private octave: number = 0;
+ private glideTime: number = 0;
+ private fineTune: number = 0;
+ private detuneAmt: number = 0;
+ private subOsc: OscillatorNode;
+ private subGain: GainNode;
constructor(id: string) {
const ctx = audioCore.getContext();
@@ -15,6 +20,15 @@ export class OscillatorWrapper {
this.output = ctx.createGain();
this.output.gain.value = 1;
+ this.subOsc = ctx.createOscillator();
+ this.subOsc.type = 'square';
+ this.subOsc.frequency.value = 220;
+ this.subOsc.start();
+ this.subGain = ctx.createGain();
+ this.subGain.gain.value = 0;
+ this.subOsc.connect(this.subGain);
+ this.subGain.connect(this.output);
+
this.node.connect(this.output);
audioCore.registerNode(id, this.output);
@@ -28,12 +42,23 @@ export class OscillatorWrapper {
public setFrequency(freq: number) {
this.baseFreq = freq;
- this.node.frequency.setValueAtTime(this.baseFreq * Math.pow(2, this.octave), audioCore.getContext().currentTime);
+ this.node.frequency.linearRampToValueAtTime(this.baseFreq * Math.pow(2, this.octave), audioCore.getContext().currentTime + this.glideTime);
+ this.subOsc.frequency.linearRampToValueAtTime(this.baseFreq * Math.pow(2, this.octave - 1), audioCore.getContext().currentTime + this.glideTime);
+ this.subOsc.frequency.linearRampToValueAtTime(this.baseFreq * Math.pow(2, this.octave - 1), audioCore.getContext().currentTime + this.glideTime);
}
public setOctave(oct: number) {
this.octave = oct;
- this.node.frequency.setValueAtTime(this.baseFreq * Math.pow(2, this.octave), audioCore.getContext().currentTime);
+ this.node.frequency.linearRampToValueAtTime(this.baseFreq * Math.pow(2, this.octave), audioCore.getContext().currentTime + this.glideTime);
+ this.subOsc.frequency.linearRampToValueAtTime(this.baseFreq * Math.pow(2, this.octave - 1), audioCore.getContext().currentTime + this.glideTime);
+ }
+
+ public setGlideTime(time: number) {
+ this.glideTime = time;
+ }
+
+ public setSubOscEnabled(enabled: boolean) {
+ this.subGain.gain.setValueAtTime(enabled ? 0.5 : 0, audioCore.getContext().currentTime);
}
public setInvertPhase(invert: boolean) {
@@ -41,11 +66,23 @@ export class OscillatorWrapper {
}
public setDetune(cents: number) {
- this.node.detune.setValueAtTime(cents, audioCore.getContext().currentTime);
+ this.detuneAmt = cents;
+ this.updateDetune();
+ }
+
+ public setFineTune(cents: number) {
+ this.fineTune = cents;
+ this.updateDetune();
+ }
+
+ private updateDetune() {
+ this.node.detune.setValueAtTime(this.detuneAmt + this.fineTune, audioCore.getContext().currentTime);
}
public destroy(id: string) {
- this.node.stop();
+this.node.stop();
+ if (this.subOsc) { this.subOsc.stop(); this.subOsc.disconnect(); }
+ if (this.subGain) { this.subGain.disconnect(); }
this.node.disconnect();
this.output.disconnect();
audioCore.unregisterNode(id);
@@ -75,7 +112,7 @@ export class NoiseWrapper {
private generateAndPlayNoise() {
if (this.node) {
- this.node.stop();
+ this.node.stop();
this.node.disconnect();
}
@@ -126,7 +163,7 @@ export class NoiseWrapper {
public destroy(id: string) {
if (this.node) {
- this.node.stop();
+ this.node.stop();
this.node.disconnect();
}
this.output.disconnect();
@@ -172,7 +209,7 @@ export class LfoWrapper {
}
public destroy(id: string) {
- this.node.stop();
+this.node.stop();
this.node.disconnect();
this.depthNode.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 bcc26963..cea630c0 100644
--- a/projects/web-synth-9x1a/src/components/nodes/BitcrusherNode.tsx
+++ b/projects/web-synth-9x1a/src/components/nodes/BitcrusherNode.tsx
@@ -40,7 +40,10 @@ export function BitcrusherNode({ id, data }: { id: string, data: Record