fix: eliminate audio timing jitter and rhythm drift
Root causes fixed: - Sequencer: replaced setTimeout note-off with Tone.Transport.scheduleOnce for sample-accurate timing instead of main-thread-dependent setTimeout - Sequencer + PianoRoll: decoupled visual updates from audio callbacks. Audio clock only writes to refs, RAF loop reads refs for visual step indicator. No more React setState inside Tone.Clock callbacks. - audioEngine: added connection lookup cache (Map) to replace O(n²) array iterations in setSequencerSignals/triggerKeyboard. Cache rebuilds lazily only when connections change. These changes eliminate the feedback loop where: audio callback → setState → React render → main thread blocks → setTimeout delayed → note-off late → drift compounds Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -363,31 +363,46 @@ export function updateParam(moduleId, paramName, value) {
|
||||
}
|
||||
}
|
||||
|
||||
// Cache connection lookups for hot-path audio scheduling
|
||||
// Rebuilt lazily when connections change
|
||||
let _connCacheVersion = -1;
|
||||
const _connByModulePort = new Map(); // "moduleId-portName" → [connections]
|
||||
|
||||
function getConnectionsFrom(moduleId, portName) {
|
||||
// Rebuild cache if connections changed
|
||||
const version = state.connections.length + state.connections.reduce((s, c) => s + c.id, 0);
|
||||
if (version !== _connCacheVersion) {
|
||||
_connByModulePort.clear();
|
||||
for (const conn of state.connections) {
|
||||
const key = `${conn.from.moduleId}-${conn.from.port}`;
|
||||
if (!_connByModulePort.has(key)) _connByModulePort.set(key, []);
|
||||
_connByModulePort.get(key).push(conn);
|
||||
}
|
||||
_connCacheVersion = version;
|
||||
}
|
||||
return _connByModulePort.get(`${moduleId}-${portName}`) || [];
|
||||
}
|
||||
|
||||
export function setSequencerSignals(moduleId, freq, gate) {
|
||||
const entry = audioNodes[moduleId];
|
||||
if (!entry) return;
|
||||
if (entry._freqSig) entry._freqSig.value = freq;
|
||||
if (entry._gateSig) entry._gateSig.value = gate ? 1 : 0;
|
||||
|
||||
// Directly set connected oscillator frequencies (bypasses the modulation Gain)
|
||||
for (const conn of state.connections) {
|
||||
if (conn.from.moduleId === moduleId && conn.from.port === 'freq') {
|
||||
const oscEntry = audioNodes[conn.to.moduleId];
|
||||
const oscMod = state.modules.find(m => m.id === conn.to.moduleId);
|
||||
if (oscEntry?.node && oscMod?.type === 'oscillator') {
|
||||
oscEntry.node.frequency.value = freq;
|
||||
}
|
||||
// Set connected oscillator frequencies directly
|
||||
for (const conn of getConnectionsFrom(moduleId, 'freq')) {
|
||||
const oscEntry = audioNodes[conn.to.moduleId];
|
||||
if (oscEntry?.node?.frequency) {
|
||||
oscEntry.node.frequency.value = freq;
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger connected envelopes
|
||||
for (const conn of state.connections) {
|
||||
if (conn.from.moduleId === moduleId && conn.from.port === 'gate') {
|
||||
const envEntry = audioNodes[conn.to.moduleId];
|
||||
if (envEntry && envEntry.node instanceof Tone.Envelope) {
|
||||
if (gate) envEntry.node.triggerAttack();
|
||||
else envEntry.node.triggerRelease();
|
||||
}
|
||||
for (const conn of getConnectionsFrom(moduleId, 'gate')) {
|
||||
const envEntry = audioNodes[conn.to.moduleId];
|
||||
if (envEntry && envEntry.node instanceof Tone.Envelope) {
|
||||
if (gate) envEntry.node.triggerAttack();
|
||||
else envEntry.node.triggerRelease();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -398,25 +413,20 @@ export function triggerKeyboard(moduleId, freq, gate) {
|
||||
if (entry._freqSig) entry._freqSig.value = freq;
|
||||
if (entry._gateSig) entry._gateSig.value = gate ? 1 : 0;
|
||||
|
||||
// Directly set connected oscillator frequencies (bypasses the modulation Gain)
|
||||
for (const conn of state.connections) {
|
||||
if (conn.from.moduleId === moduleId && conn.from.port === 'freq') {
|
||||
const oscEntry = audioNodes[conn.to.moduleId];
|
||||
const oscMod = state.modules.find(m => m.id === conn.to.moduleId);
|
||||
if (oscEntry?.node && oscMod?.type === 'oscillator') {
|
||||
oscEntry.node.frequency.value = freq;
|
||||
}
|
||||
// Set connected oscillator frequencies directly
|
||||
for (const conn of getConnectionsFrom(moduleId, 'freq')) {
|
||||
const oscEntry = audioNodes[conn.to.moduleId];
|
||||
if (oscEntry?.node?.frequency) {
|
||||
oscEntry.node.frequency.value = freq;
|
||||
}
|
||||
}
|
||||
|
||||
// Also trigger any connected envelopes
|
||||
for (const conn of state.connections) {
|
||||
if (conn.from.moduleId === moduleId && conn.from.port === 'gate') {
|
||||
const envEntry = audioNodes[conn.to.moduleId];
|
||||
if (envEntry && envEntry.node instanceof Tone.Envelope) {
|
||||
if (gate) envEntry.node.triggerAttack();
|
||||
else envEntry.node.triggerRelease();
|
||||
}
|
||||
// Trigger connected envelopes
|
||||
for (const conn of getConnectionsFrom(moduleId, 'gate')) {
|
||||
const envEntry = audioNodes[conn.to.moduleId];
|
||||
if (envEntry && envEntry.node instanceof Tone.Envelope) {
|
||||
if (gate) envEntry.node.triggerAttack();
|
||||
else envEntry.node.triggerRelease();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user