diff --git a/projects/web-synth-9x1a/JOURNAL.md b/projects/web-synth-9x1a/JOURNAL.md index b258e80a..097010ba 100644 --- a/projects/web-synth-9x1a/JOURNAL.md +++ b/projects/web-synth-9x1a/JOURNAL.md @@ -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 diff --git a/projects/web-synth-9x1a/src/audio/nodes/processors.ts b/projects/web-synth-9x1a/src/audio/nodes/processors.ts index e6767991..cec351a6 100644 --- a/projects/web-synth-9x1a/src/audio/nodes/processors.ts +++ b/projects/web-synth-9x1a/src/audio/nodes/processors.ts @@ -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(); @@ -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) { diff --git a/projects/web-synth-9x1a/src/audio/nodes/sources.ts b/projects/web-synth-9x1a/src/audio/nodes/sources.ts index a4d645cb..efc60f28 100644 --- a/projects/web-synth-9x1a/src/audio/nodes/sources.ts +++ b/projects/web-synth-9x1a/src/audio/nodes/sources.ts @@ -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); } } diff --git a/projects/web-synth-9x1a/src/components/nodes/GainNode.tsx b/projects/web-synth-9x1a/src/components/nodes/GainNode.tsx index 9d243339..9b5a297c 100644 --- a/projects/web-synth-9x1a/src/components/nodes/GainNode.tsx +++ b/projects/web-synth-9x1a/src/components/nodes/GainNode.tsx @@ -28,8 +28,17 @@ export function GainNode({ id, data }: { id: string, data: Record }
+ +
diff --git a/projects/web-synth-9x1a/src/components/nodes/NoiseNode.tsx b/projects/web-synth-9x1a/src/components/nodes/NoiseNode.tsx index 860dd8b4..0c7952e3 100644 --- a/projects/web-synth-9x1a/src/components/nodes/NoiseNode.tsx +++ b/projects/web-synth-9x1a/src/components/nodes/NoiseNode.tsx @@ -25,7 +25,20 @@ export function NoiseNode({ id, data }: { id: string, data: Record -
White Noise
+
+ +
); diff --git a/projects/web-synth-9x1a/src/components/nodes/OscillatorNode.tsx b/projects/web-synth-9x1a/src/components/nodes/OscillatorNode.tsx index 4db8ebb3..0577b14c 100644 --- a/projects/web-synth-9x1a/src/components/nodes/OscillatorNode.tsx +++ b/projects/web-synth-9x1a/src/components/nodes/OscillatorNode.tsx @@ -63,6 +63,13 @@ export function OscillatorNode({ id, data }: { id: string, data: Record + + diff --git a/projects/web-synth-9x1a/src/store.ts b/projects/web-synth-9x1a/src/store.ts index 28b61ec5..e2b4e948 100644 --- a/projects/web-synth-9x1a/src/store.ts +++ b/projects/web-synth-9x1a/src/store.ts @@ -233,8 +233,11 @@ export const useStore = create((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);