diff --git a/packages/client/src/components/ModuleNode.jsx b/packages/client/src/components/ModuleNode.jsx index a5bdd03..768eb45 100644 --- a/packages/client/src/components/ModuleNode.jsx +++ b/packages/client/src/components/ModuleNode.jsx @@ -60,7 +60,7 @@ export default function ModuleNode({ mod, zoom, onStartConnect, onPortPosition } } } - // ==================== Live modulation visualization (LFO + Envelope + any CV) ==================== + // ==================== Live modulation visualization (any source → any param) ==================== const [liveValues, setLiveValues] = useState({}); const rafRef = useRef(null); const startTimeRef = useRef(performance.now() / 1000); @@ -80,6 +80,13 @@ export default function ModuleNode({ mod, zoom, onStartConnect, onPortPosition } const t = performance.now() / 1000 - startTimeRef.current; const newValues = {}; + // Read current params fresh from state each tick (avoid stale closure) + const curMod = state.modules.find(m => m.id === mod.id); + if (!curMod) return; + const curDef = getModuleDef(curMod.type); + if (!curDef) return; + const curParams = { ...Object.fromEntries(Object.entries(curDef.params).map(([k, v]) => [k, v.default])), ...curMod.params }; + for (const conn of state.connections) { if (conn.to.moduleId !== mod.id) continue; const paramName = portMap[conn.to.port]; @@ -88,40 +95,55 @@ export default function ModuleNode({ mod, zoom, onStartConnect, onPortPosition } const srcMod = state.modules.find(m => m.id === conn.from.moduleId); if (!srcMod) continue; + const baseValue = curParams[paramName]; + + // Modulation scale based on target parameter + const getScale = () => { + if (curMod.type === 'oscillator' && paramName === 'frequency') return baseValue * 0.5; + if (curMod.type === 'filter' && paramName === 'frequency') return baseValue; + if (curMod.type === 'vca' && paramName === 'gain') return 1; + return baseValue || 1; + }; + if (srcMod.type === 'lfo') { // LFO: simulate waveform for smooth visual const lfoDef = getModuleDef('lfo'); const lfoP = { ...Object.fromEntries(Object.entries(lfoDef.params).map(([k, v]) => [k, v.default])), ...srcMod.params }; - const freq = lfoP.frequency; - const amp = lfoP.amplitude; - const waveform = lfoP.waveform; - const phase = (t * freq) % 1; - const lfoVal = simulateLFO(waveform, phase) * amp; + const phase = (t * lfoP.frequency) % 1; + const lfoVal = simulateLFO(lfoP.waveform, phase) * lfoP.amplitude; + newValues[paramName] = baseValue + lfoVal * getScale(); - const baseValue = params[paramName]; - let scale; - if (mod.type === 'oscillator' && paramName === 'frequency') scale = baseValue * 0.5; - else if (mod.type === 'filter' && paramName === 'frequency') scale = baseValue; - else if (mod.type === 'vca' && paramName === 'gain') scale = 1; - else scale = baseValue || 1; - - newValues[paramName] = baseValue + lfoVal * scale; } else if (srcMod.type === 'envelope') { - // Envelope: read the envelope's current level (0-1) from the source module + // Envelope: read current level (0-1) from the source envelope node const envEntry = getAudioNode(srcMod.id); if (envEntry?.node) { const envValue = typeof envEntry.node.value === 'number' ? envEntry.node.value : 0; - const baseValue = params[paramName]; - if (mod.type === 'vca' && paramName === 'gain') { + if (curMod.type === 'vca' && paramName === 'gain') { newValues[paramName] = envValue; // Envelope directly drives gain (0→1) } else { - let scale; - if (mod.type === 'oscillator' && paramName === 'frequency') scale = baseValue * 0.5; - else if (mod.type === 'filter' && paramName === 'frequency') scale = baseValue; - else scale = baseValue || 1; - newValues[paramName] = baseValue + envValue * scale; + newValues[paramName] = baseValue + envValue * getScale(); } } + + } else if (srcMod.type === 'oscillator') { + // Oscillator FM: simulate modulating oscillator waveform + const srcDef = getModuleDef('oscillator'); + const srcP = { ...Object.fromEntries(Object.entries(srcDef.params).map(([k, v]) => [k, v.default])), ...srcMod.params }; + // Clamp visual frequency to avoid aliasing — show a slow representation + const visFreq = Math.min(srcP.frequency, 8); + const phase = (t * visFreq) % 1; + const modVal = simulateLFO(srcP.waveform, phase) * 0.5; + newValues[paramName] = baseValue + modVal * getScale(); + + } else if (srcMod.type === 'noise') { + // Noise: random jitter + const noiseVal = (Math.random() * 2 - 1) * 0.3; + newValues[paramName] = baseValue + noiseVal * getScale(); + + } else { + // Generic fallback: subtle visual pulse so user sees modulation is active + const pulseVal = Math.sin(2 * Math.PI * t) * 0.2; + newValues[paramName] = baseValue + pulseVal * getScale(); } }