diff --git a/src/components/ModuleNode.jsx b/src/components/ModuleNode.jsx index 6285969..3f171e0 100644 --- a/src/components/ModuleNode.jsx +++ b/src/components/ModuleNode.jsx @@ -1,7 +1,7 @@ import React, { useCallback, useState, useEffect, useRef } from 'react'; import { getModuleDef } from '../engine/moduleRegistry.js'; import { state, removeModule, updateModuleParam, isPortConnected, emit } from '../engine/state.js'; -import { updateParam } from '../engine/audioEngine.js'; +import { updateParam, getAudioNode } from '../engine/audioEngine.js'; import Knob from './Knob.jsx'; import ScopeDisplay from './ScopeDisplay.jsx'; import KeyboardWidget from './KeyboardWidget.jsx'; @@ -60,7 +60,7 @@ export default function ModuleNode({ mod, zoom, onStartConnect, onPortPosition } } } - // ==================== Live LFO modulation visualization ==================== + // ==================== Live modulation visualization (LFO + Envelope + any CV) ==================== const [liveValues, setLiveValues] = useState({}); const rafRef = useRef(null); const startTimeRef = useRef(performance.now() / 1000); @@ -75,7 +75,6 @@ export default function ModuleNode({ mod, zoom, onStartConnect, onPortPosition } const tick = () => { frameCount++; rafRef.current = requestAnimationFrame(tick); - // Throttle to ~15fps (every 4th frame) to reduce main thread pressure if (frameCount % 4 !== 0) return; const t = performance.now() / 1000 - startTimeRef.current; @@ -87,26 +86,34 @@ export default function ModuleNode({ mod, zoom, onStartConnect, onPortPosition } if (!paramName) continue; const srcMod = state.modules.find(m => m.id === conn.from.moduleId); - if (!srcMod || srcMod.type !== 'lfo') continue; + if (!srcMod) continue; - // Read LFO params from state - 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; + 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; - // Compute modulated value (same scaling as audioEngine) - 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; + 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; + newValues[paramName] = baseValue + lfoVal * scale; + } else if (srcMod.type === 'envelope') { + // Envelope: read the actual audio node gain value for real-time display + const audioEntry = getAudioNode(mod.id); + if (audioEntry?.node?.gain) { + const currentGain = audioEntry.node.gain.value; + newValues[paramName] = currentGain; + } + } } setLiveValues(newValues); diff --git a/src/engine/audioEngine.js b/src/engine/audioEngine.js index 0ef8391..ad5b387 100644 --- a/src/engine/audioEngine.js +++ b/src/engine/audioEngine.js @@ -327,6 +327,11 @@ export function connectWire(conn) { } catch (e) { console.warn('connect error', e); } + + // When CV is connected to VCA, zero the base gain so only envelope controls it + if (toMod?.type === 'vca' && conn.to.port === 'cv') { + toEntry.node.gain.value = 0; + } } export function disconnectWire(conn) { @@ -345,6 +350,12 @@ export function disconnectWire(conn) { } catch (e) { // Tone.js may throw if not connected } + + // When CV is disconnected from VCA, restore base gain from params + const toMod = state.modules.find(m => m.id === conn.to.moduleId); + if (toMod?.type === 'vca' && conn.to.port === 'cv') { + toEntry.node.gain.value = toMod.params?.gain ?? 0.8; + } } export function updateParam(moduleId, paramName, value) {