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 { 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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user