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
13 changes: 13 additions & 0 deletions projects/web-synth-9x1a/JOURNAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
51 changes: 44 additions & 7 deletions projects/web-synth-9x1a/src/audio/nodes/sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
Expand All @@ -28,24 +42,47 @@ 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) {
this.output.gain.setValueAtTime(invert ? -1 : 1, audioCore.getContext().currentTime);
}

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);
Expand Down Expand Up @@ -75,7 +112,7 @@ export class NoiseWrapper {

private generateAndPlayNoise() {
if (this.node) {
this.node.stop();
this.node.stop();
this.node.disconnect();
}

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ export function BitcrusherNode({ id, data }: { id: string, data: Record<string,
/>
</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 className="flex gap-1 mt-1">
<button onClick={() => updateNodeData(id, { bits: 8, mix: 1.0, bypass: false })} className="flex-1 bg-gray-700 hover:bg-gray-600 text-[10px] py-1 rounded text-gray-300 transition-colors">Reset</button>
<button onClick={() => updateNodeData(id, { bits: Math.floor(Math.random() * 16) + 1, mix: Math.random(), bypass: false })} className="flex-1 bg-gray-700 hover:bg-gray-600 text-[10px] py-1 rounded text-gray-300 transition-colors">Randomize</button>
</div>
</div>

<Handle type="target" position={Position.Left} id="in" className="w-3 h-3 bg-orange-500" />
Expand Down
40 changes: 38 additions & 2 deletions projects/web-synth-9x1a/src/components/nodes/OscillatorNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ export function OscillatorNode({ id, data }: { id: string, data: Record<string,
className="ml-2 accent-blue-500"
/>
</label>
<label className="text-xs text-gray-300 flex items-center justify-between mb-1">
Sub-Oscillator
<input
type="checkbox"
checked={data.subOscEnabled || false}
onChange={(e) => updateNodeData(id, { subOscEnabled: e.target.checked })}
className="ml-2 accent-blue-500"
/>
</label>

<label className="text-xs text-gray-300 flex flex-col">
Type
<select
Expand Down Expand Up @@ -72,6 +82,17 @@ export function OscillatorNode({ id, data }: { id: string, data: Record<string,
className="mt-1"
/>
</label>
<label className="text-xs text-gray-300 flex flex-col">
Fine Tune: {data.fineTune || 0} cents
<input
type="range"
min="-100" max="100" step="1"
value={data.fineTune || 0}
onChange={(e) => updateNodeData(id, { fineTune: Number(e.target.value) })}
className="mt-1"
/>
</label>


<label className="text-xs text-gray-300 flex flex-col">
Octave
Expand All @@ -88,9 +109,21 @@ export function OscillatorNode({ id, data }: { id: string, data: Record<string,
</select>
</label>


<label className="text-xs text-gray-300 flex flex-col">
Glide: {data.glideTime || 0}s
<input
type="range"
min="0" max="2" step="0.01"
value={data.glideTime || 0}
onChange={(e) => updateNodeData(id, { glideTime: Number(e.target.value) })}
className="mt-1"
/>
</label>

<div className="flex gap-1 mt-1">
<button
onClick={() => updateNodeData(id, { frequency: 440, type: 'sawtooth', detune: 0, octave: 0, invertPhase: false })}
onClick={() => updateNodeData(id, { frequency: 440, type: 'sawtooth', detune: 0, octave: 0, invertPhase: false, glideTime: 0, subOscEnabled: false, fineTune: 0 })}
className="flex-1 bg-gray-700 hover:bg-gray-600 text-[10px] py-1 rounded text-gray-300 transition-colors"
>
Reset
Expand All @@ -103,7 +136,10 @@ export function OscillatorNode({ id, data }: { id: string, data: Record<string,
type: types[Math.floor(Math.random() * types.length)],
detune: Math.floor(Math.random() * 2400 - 1200),
octave: Math.floor(Math.random() * 5 - 2),
invertPhase: Math.random() > 0.5
invertPhase: Math.random() > 0.5,
glideTime: Math.round(Math.random() * 200) / 100,
subOscEnabled: Math.random() > 0.5,
fineTune: Math.floor(Math.random() * 200 - 100)
});
}}
className="flex-1 bg-gray-700 hover:bg-gray-600 text-[10px] py-1 rounded text-gray-300 transition-colors"
Expand Down
3 changes: 3 additions & 0 deletions projects/web-synth-9x1a/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,9 @@ export const useStore = create<AppState>((set, get) => ({
if (data.detune !== undefined) wrapper.setDetune(data.detune);
if (data.octave !== undefined) wrapper.setOctave(data.octave);
if (data.invertPhase !== undefined) wrapper.setInvertPhase(data.invertPhase);
if (data.glideTime !== undefined) wrapper.setGlideTime(data.glideTime);
if (data.subOscEnabled !== undefined) wrapper.setSubOscEnabled(data.subOscEnabled);
if (data.fineTune !== undefined) wrapper.setFineTune(data.fineTune);
} else if (wrapper instanceof LfoWrapper) {
if (data.frequency !== undefined) wrapper.setFrequency(data.frequency);
if (data.type !== undefined) wrapper.setType(data.type);
Expand Down