fix: VCA zeroes on live CV connect + visual feedback for envelope control
VCA fix: - connectWire now zeros VCA base gain immediately when CV is connected (previously only rebuildGraph did this, missing live-connect case) - disconnectWire restores base gain from params when CV is removed Visual modulation feedback: - ModuleNode RAF loop now handles envelope sources (not just LFO) - Reads actual Tone.js gain node value for real-time display - VCA gain knob shows live envelope value during playback - LFO visualization unchanged (simulated waveform as before) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import React, { useCallback, useState, useEffect, useRef } from 'react';
|
import React, { useCallback, useState, useEffect, useRef } from 'react';
|
||||||
import { getModuleDef } from '../engine/moduleRegistry.js';
|
import { getModuleDef } from '../engine/moduleRegistry.js';
|
||||||
import { state, removeModule, updateModuleParam, isPortConnected, emit } from '../engine/state.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 Knob from './Knob.jsx';
|
||||||
import ScopeDisplay from './ScopeDisplay.jsx';
|
import ScopeDisplay from './ScopeDisplay.jsx';
|
||||||
import KeyboardWidget from './KeyboardWidget.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 [liveValues, setLiveValues] = useState({});
|
||||||
const rafRef = useRef(null);
|
const rafRef = useRef(null);
|
||||||
const startTimeRef = useRef(performance.now() / 1000);
|
const startTimeRef = useRef(performance.now() / 1000);
|
||||||
@@ -75,7 +75,6 @@ export default function ModuleNode({ mod, zoom, onStartConnect, onPortPosition }
|
|||||||
const tick = () => {
|
const tick = () => {
|
||||||
frameCount++;
|
frameCount++;
|
||||||
rafRef.current = requestAnimationFrame(tick);
|
rafRef.current = requestAnimationFrame(tick);
|
||||||
// Throttle to ~15fps (every 4th frame) to reduce main thread pressure
|
|
||||||
if (frameCount % 4 !== 0) return;
|
if (frameCount % 4 !== 0) return;
|
||||||
|
|
||||||
const t = performance.now() / 1000 - startTimeRef.current;
|
const t = performance.now() / 1000 - startTimeRef.current;
|
||||||
@@ -87,26 +86,34 @@ export default function ModuleNode({ mod, zoom, onStartConnect, onPortPosition }
|
|||||||
if (!paramName) continue;
|
if (!paramName) continue;
|
||||||
|
|
||||||
const srcMod = state.modules.find(m => m.id === conn.from.moduleId);
|
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
|
if (srcMod.type === 'lfo') {
|
||||||
const lfoDef = getModuleDef('lfo');
|
// LFO: simulate waveform for smooth visual
|
||||||
const lfoP = { ...Object.fromEntries(Object.entries(lfoDef.params).map(([k, v]) => [k, v.default])), ...srcMod.params };
|
const lfoDef = getModuleDef('lfo');
|
||||||
const freq = lfoP.frequency;
|
const lfoP = { ...Object.fromEntries(Object.entries(lfoDef.params).map(([k, v]) => [k, v.default])), ...srcMod.params };
|
||||||
const amp = lfoP.amplitude;
|
const freq = lfoP.frequency;
|
||||||
const waveform = lfoP.waveform;
|
const amp = lfoP.amplitude;
|
||||||
const phase = (t * freq) % 1;
|
const waveform = lfoP.waveform;
|
||||||
const lfoVal = simulateLFO(waveform, phase) * amp;
|
const phase = (t * freq) % 1;
|
||||||
|
const lfoVal = simulateLFO(waveform, phase) * amp;
|
||||||
|
|
||||||
// Compute modulated value (same scaling as audioEngine)
|
const baseValue = params[paramName];
|
||||||
const baseValue = params[paramName];
|
let scale;
|
||||||
let scale;
|
if (mod.type === 'oscillator' && paramName === 'frequency') scale = baseValue * 0.5;
|
||||||
if (mod.type === 'oscillator' && paramName === 'frequency') scale = baseValue * 0.5;
|
else if (mod.type === 'filter' && paramName === 'frequency') scale = baseValue;
|
||||||
else if (mod.type === 'filter' && paramName === 'frequency') scale = baseValue;
|
else if (mod.type === 'vca' && paramName === 'gain') scale = 1;
|
||||||
else if (mod.type === 'vca' && paramName === 'gain') scale = 1;
|
else scale = baseValue || 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);
|
setLiveValues(newValues);
|
||||||
|
|||||||
@@ -327,6 +327,11 @@ export function connectWire(conn) {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('connect error', 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) {
|
export function disconnectWire(conn) {
|
||||||
@@ -345,6 +350,12 @@ export function disconnectWire(conn) {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Tone.js may throw if not connected
|
// 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) {
|
export function updateParam(moduleId, paramName, value) {
|
||||||
|
|||||||
Reference in New Issue
Block a user