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 [liveValues, setLiveValues] = useState({});
|
||||||
const rafRef = useRef(null);
|
const rafRef = useRef(null);
|
||||||
const startTimeRef = useRef(performance.now() / 1000);
|
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 t = performance.now() / 1000 - startTimeRef.current;
|
||||||
const newValues = {};
|
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) {
|
for (const conn of state.connections) {
|
||||||
if (conn.to.moduleId !== mod.id) continue;
|
if (conn.to.moduleId !== mod.id) continue;
|
||||||
const paramName = portMap[conn.to.port];
|
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);
|
const srcMod = state.modules.find(m => m.id === conn.from.moduleId);
|
||||||
if (!srcMod) continue;
|
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') {
|
if (srcMod.type === 'lfo') {
|
||||||
// LFO: simulate waveform for smooth visual
|
// LFO: simulate waveform for smooth visual
|
||||||
const lfoDef = getModuleDef('lfo');
|
const lfoDef = getModuleDef('lfo');
|
||||||
const lfoP = { ...Object.fromEntries(Object.entries(lfoDef.params).map(([k, v]) => [k, v.default])), ...srcMod.params };
|
const lfoP = { ...Object.fromEntries(Object.entries(lfoDef.params).map(([k, v]) => [k, v.default])), ...srcMod.params };
|
||||||
const freq = lfoP.frequency;
|
const phase = (t * lfoP.frequency) % 1;
|
||||||
const amp = lfoP.amplitude;
|
const lfoVal = simulateLFO(lfoP.waveform, phase) * lfoP.amplitude;
|
||||||
const waveform = lfoP.waveform;
|
newValues[paramName] = baseValue + lfoVal * getScale();
|
||||||
const phase = (t * freq) % 1;
|
|
||||||
const lfoVal = simulateLFO(waveform, phase) * amp;
|
|
||||||
|
|
||||||
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') {
|
} 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);
|
const envEntry = getAudioNode(srcMod.id);
|
||||||
if (envEntry?.node) {
|
if (envEntry?.node) {
|
||||||
const envValue = typeof envEntry.node.value === 'number' ? envEntry.node.value : 0;
|
const envValue = typeof envEntry.node.value === 'number' ? envEntry.node.value : 0;
|
||||||
const baseValue = params[paramName];
|
if (curMod.type === 'vca' && paramName === 'gain') {
|
||||||
if (mod.type === 'vca' && paramName === 'gain') {
|
|
||||||
newValues[paramName] = envValue; // Envelope directly drives gain (0→1)
|
newValues[paramName] = envValue; // Envelope directly drives gain (0→1)
|
||||||
} else {
|
} else {
|
||||||
let scale;
|
newValues[paramName] = baseValue + envValue * getScale();
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} 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