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 @@ -77,3 +77,16 @@
- [ ] Implement auto-save to localStorage
- [ ] Add tooltips and visual guides for new users
- [ ] Add stereo widening utility node


## Expansion Phase 7 (New Additions)
- [x] Add Pink Noise support to Noise Node
- [x] Add Brown Noise support to Noise Node
- [x] Add Mute toggle to Gain Node
- [ ] Add a Bypass switch to Filter Node
- [ ] Add visual metronome toggle for Sequencer
- [ ] Implement Node duplication shortcut (Cmd/Ctrl + D)
- [ ] Add Global Tempo state
- [ ] Add Volume meter to Output Node
- [ ] Implement node collapsing/folding
- [x] Add a "Reset to Default" button on nodes
16 changes: 15 additions & 1 deletion projects/web-synth-9x1a/src/audio/nodes/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { audioCore } from '../core';

export class GainWrapper {
public node: GainNode;
private currentGain: number = 0.5;
private isMuted: boolean = false;

constructor(id: string) {
const ctx = audioCore.getContext();
Expand All @@ -13,7 +15,19 @@ export class GainWrapper {
}

public setGain(value: number) {
this.node.gain.setValueAtTime(value, audioCore.getContext().currentTime);
this.currentGain = value;
if (!this.isMuted) {
this.node.gain.setValueAtTime(value, audioCore.getContext().currentTime);
}
}

public setMute(mute: boolean) {
this.isMuted = mute;
if (this.isMuted) {
this.node.gain.setValueAtTime(0, audioCore.getContext().currentTime);
} else {
this.node.gain.setValueAtTime(this.currentGain, audioCore.getContext().currentTime);
}
}

public destroy(id: string) {
Expand Down
76 changes: 63 additions & 13 deletions projects/web-synth-9x1a/src/audio/nodes/sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,30 +37,80 @@ export class OscillatorWrapper {

export class NoiseWrapper {
private bufferSize: number;
public node: AudioBufferSourceNode;
private buffer: AudioBuffer;
public node: AudioBufferSourceNode | null = null;
private output: GainNode;
private type: 'white' | 'pink' | 'brown' = 'white';
private ctx: AudioContext;

constructor(id: string) {
const ctx = audioCore.getContext();
this.bufferSize = ctx.sampleRate * 2; // 2 seconds of noise
this.buffer = ctx.createBuffer(1, this.bufferSize, ctx.sampleRate);
this.ctx = audioCore.getContext();
this.bufferSize = this.ctx.sampleRate * 2; // 2 seconds of noise

this.output = this.ctx.createGain();
this.output.gain.value = 1.0;

audioCore.registerNode(id, this.output);

this.generateAndPlayNoise();
}

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

// Fill buffer with white noise
const output = this.buffer.getChannelData(0);
for (let i = 0; i < this.bufferSize; i++) {
output[i] = Math.random() * 2 - 1;
const buffer = this.ctx.createBuffer(1, this.bufferSize, this.ctx.sampleRate);
const outputData = buffer.getChannelData(0);

if (this.type === 'white') {
for (let i = 0; i < this.bufferSize; i++) {
outputData[i] = Math.random() * 2 - 1;
}
} else if (this.type === 'pink') {
let b0 = 0, b1 = 0, b2 = 0, b3 = 0, b4 = 0, b5 = 0, b6 = 0;
for (let i = 0; i < this.bufferSize; i++) {
const white = Math.random() * 2 - 1;
b0 = 0.99886 * b0 + white * 0.0555179;
b1 = 0.99332 * b1 + white * 0.0750759;
b2 = 0.96900 * b2 + white * 0.1538520;
b3 = 0.86650 * b3 + white * 0.3104856;
b4 = 0.55000 * b4 + white * 0.5329522;
b5 = -0.7616 * b5 - white * 0.0168980;
outputData[i] = b0 + b1 + b2 + b3 + b4 + b5 + b6 + white * 0.5362;
outputData[i] *= 0.11; // compensation
b6 = white * 0.115926;
}
} else if (this.type === 'brown') {
let lastOut = 0;
for (let i = 0; i < this.bufferSize; i++) {
const white = Math.random() * 2 - 1;
outputData[i] = (lastOut + (0.02 * white)) / 1.02;
lastOut = outputData[i];
outputData[i] *= 3.5; // compensate gain
}
}

this.node = ctx.createBufferSource();
this.node.buffer = this.buffer;
this.node = this.ctx.createBufferSource();
this.node.buffer = buffer;
this.node.loop = true;
this.node.connect(this.output);
this.node.start();
}

audioCore.registerNode(id, this.node);
public setType(type: 'white' | 'pink' | 'brown') {
if (this.type !== type) {
this.type = type;
this.generateAndPlayNoise();
}
}

public destroy(id: string) {
this.node.stop();
if (this.node) {
this.node.stop();
this.node.disconnect();
}
this.output.disconnect();
audioCore.unregisterNode(id);
}
}
Expand Down
17 changes: 16 additions & 1 deletion projects/web-synth-9x1a/src/components/nodes/GainNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,17 @@ export function GainNode({ 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">
Mute
<input
type="checkbox"
checked={data.muted || false}
onChange={(e) => updateNodeData(id, { muted: e.target.checked })}
className="ml-2 accent-green-500"
/>
</label>
<label className="text-xs text-gray-300 flex flex-col">
Level: {data.gain?.toFixed(2)}
Level: {data.gain !== undefined ? data.gain.toFixed(2) : '0.50'}
<input
type="range"
min="0" max="2" step="0.01"
Expand All @@ -38,6 +47,12 @@ export function GainNode({ id, data }: { id: string, data: Record<string, any> }
className="mt-1"
/>
</label>
<button
onClick={() => updateNodeData(id, { gain: 0.5, muted: 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-green-500" />
Expand Down
15 changes: 14 additions & 1 deletion projects/web-synth-9x1a/src/components/nodes/NoiseNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,20 @@ export function NoiseNode({ id, data }: { id: string, data: Record<string, any>
</div>
<button onClick={() => removeNode(id)} className="text-gray-500 hover:text-red-400"><X size={14} /></button>
</div>
<div className="text-xs text-gray-400 italic">White Noise</div>
<div className="flex flex-col gap-2 mt-2">
<label className="text-xs text-gray-300 flex flex-col">
Type
<select
value={data.type || 'white'}
onChange={(e) => updateNodeData(id, { type: e.target.value })}
className="mt-1 bg-gray-700 border border-gray-600 text-xs p-1 rounded"
>
<option value="white">White Noise</option>
<option value="pink">Pink Noise</option>
<option value="brown">Brown Noise</option>
</select>
</label>
</div>
<Handle type="source" position={Position.Right} id="out" className="w-3 h-3 bg-gray-400" />
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ export function OscillatorNode({ id, data }: { id: string, data: Record<string,
className="mt-1"
/>
</label>

<button
onClick={() => updateNodeData(id, { frequency: 440, type: 'sawtooth', detune: 0 })}
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="frequency" className="w-3 h-3 bg-blue-500" />
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 @@ -233,8 +233,11 @@ export const useStore = create<AppState>((set, get) => ({
if (data.type !== undefined) wrapper.setType(data.type);
if (data.depth !== undefined) wrapper.setDepth(data.depth);

} else if (wrapper instanceof NoiseWrapper) {
if (data.type !== undefined) wrapper.setType(data.type);
} else if (wrapper instanceof GainWrapper) {
if (data.gain !== undefined) wrapper.setGain(data.gain);
if (data.muted !== undefined) wrapper.setMute(data.muted);
} else if (wrapper instanceof CompressorWrapper) {
if (data.threshold !== undefined) wrapper.setThreshold(data.threshold);
if (data.knee !== undefined) wrapper.setKnee(data.knee);
Expand Down