feat: universal modulation animation for all source types
- Read params fresh each tick instead of stale closure - Add oscillator FM, noise, and generic fallback animations - Any modulation source now shows visual feedback on target knob Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user