Conversation
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #431 +/- ##
===========================================
- Coverage 59.81% 59.81% -0.01%
===========================================
Files 568 568
Lines 79964 79964
Branches 12381 12379 -2
===========================================
- Hits 47828 47827 -1
- Misses 32136 32137 +1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Plan for a zero-latency monitoring path for live MIDI input into synth tracks. This is an engine change (
modules/tracktion_engine+modules/tracktion_graph) - no app-side work is needed for the graph plumbing, only for whatever UI eventually toggles it.No implementation yet: the approach needs a decision from the maintainer (see Open questions), in particular whether the goal is "remove the avoidable compensation delay" or "route the monitored track straight to the output, bypassing the master bus".
Where the delay actually comes from
Live MIDI reaches the graph via
LiveMidiInjectingNode(keyboard/step entry/previews, added increateNodeForAudioTrack,tracktion_EditNodeBuilder.cpp:1592) or viaMidiInputDeviceNode/HostedMidiInputDeviceNodethroughcreateLiveInputsNode. Both are injected upstream of the track's plugin chain, so a note played now is heard after the full PDC delay. That delay has three distinct components:Edit::setLowLatencyMonitoringalready does, bluntly, by disabling them).LatencyNodes inserted to align this track's branch with more-latent sibling branches -SummingNode::createLatencyNodes(tracktion_SummingNode.h:267) and the same logic duplicated inConnectedNode(tracktion_ConnectedNode.h:272). This is pure compensation delay and is entirely avoidable for a live path.createMasterPluginsNode). Avoidable only by routing around it.The maintainer's comment on the issue proposes attacking (2) by marking the live node so compensation is skipped. That is the right first step, but it is worth being explicit that it does not address (3): a linear-phase EQ or lookahead limiter on the master bus will still delay the monitored note by its full latency.
The structural problem with "just the live path"
The issue comment asks whether the exemption can be applied to the live path only. It cannot, as the graph is currently shaped:
LiveMidiInjectingNodewraps the whole clips node, so live MIDI and timeline MIDI are merged into one buffer before reaching the synth.So the exemption necessarily applies to the whole track branch. That is only correct when the track has no timeline material that must stay in sync with the rest of the Edit - which is precisely the record-armed/input-monitored case, where clips are already muted by the
TrackMutingNodepair increateNodeForAudioTrack("When recording, clips should be muted but the plugin should still be audible"). Hence the gating question below.Option 1 - compensation-exempt branches (recommended first step)
Addresses (2). Localised, testable at the
tracktion_graphlevel, and is the foundation for anything else.Mechanism
Add an explicit
bool bypassLatencyCompensationtoNodeProperties(tracktion_Node.h:150).Prefer this over encoding it as a negative
latencyNumSamples.latencyNumSamplesis already max'd/accumulated in about eight places,std::numeric_limits<int>::min()is already used as the "no inputs yet" sentinel inSummingNode::getNodePropertiesandInsertSendNode, andsubtractNoWrapexists specifically to cope with that sentinel. A second negative meaning would be silently absorbed by thosestd::maxcalls.Propagation rule: pass-through nodes propagate the flag; merging nodes compute
latencyNumSamplesas the max over non-exempt inputs only and reportbypassLatencyCompensation = false, since the merge re-establishes a compensated stream. The merge sites are:SummingNode::getNodeProperties/createLatencyNodesConnectedNode::getNodePropertiesand its latency loopCombiningNode(tracktion_CombiningNode.cpp:164)ArrangerLauncherSwitchingNode(tracktion_ArrangerLauncherSwitchingNode.cpp:54)RackReturnNode(tracktion_RackReturnNode.cpp:44) andRackNode(tracktion_RackNode.cpp:99)ReturnNode(tracktion_TestNodes.h:568)SummingNode::createLatencyNodesand theConnectedNodeequivalent skip exempt inputs entirely (noLatencyNodewrapped around them).Set the flag in
LiveMidiInjectingNode::getNodeProperties(), and inMidiInputDeviceNode/HostedMidiInputDeviceNodefor hardware MIDI in, when the owning track opts in.Invariant that must hold: the root node's
latencyNumSamplesmust be unchanged by the presence of an exempt branch.PlayHeadPositionNode(tracktion_PlayHeadPositionNode.h:47) andEditPlaybackContext::getLatencySamples()(tracktion_EditPlaybackContext.cpp:209) both read it; if an exempt branch pulled it down, timeline sync and the host-reported latency would both break.Complexity: medium.
Option 2 - direct monitor tap
Addresses (2) and (3). The armed track's post-plugin output is sent to a dedicated monitor bus (the existing
SendNode/ReturnNodebus mechanism already used forgetWaveInputDeviceBusID/ sidechains fits) and summed in at the device output node, aftercreateMasterPluginsNode, with the track's normal contribution suppressed while monitoring.The signal cannot simply be added in parallel - the same audio would then be heard twice, once early and once delayed - so the track's normal path has to be muted for the duration, which changes what the user hears (no master-bus processing on the monitored track). That is a defensible and common "direct monitoring" behaviour, but it is a product decision, not an implementation detail.
Complexity: large.
Option 3 - hybrid
Option 1 as the default behaviour for armed tracks, Option 2 behind an explicit per-track "direct monitoring" option. Probably where this ends up, but Option 1 should land and be proven first.
Affected files
modules/tracktion_graph/tracktion_graph/tracktion_Node.h-NodePropertiesflagmodules/tracktion_graph/tracktion_graph/nodes/tracktion_SummingNode.h- props +createLatencyNodesmodules/tracktion_graph/tracktion_graph/nodes/tracktion_ConnectedNode.h- same, for the multi-threaded playermodules/tracktion_graph/tracktion_graph/tracktion_TestNodes.h-ReturnNode, test summing nodesmodules/tracktion_engine/playback/graph/tracktion_LiveMidiInjectingNode.{h,cpp}- set the flagmodules/tracktion_engine/playback/graph/tracktion_MidiInputDeviceNode.{h,cpp},tracktion_HostedMidiInputDeviceNode.{h,cpp}- set the flagmodules/tracktion_engine/playback/graph/tracktion_CombiningNode.cpp,tracktion_ArrangerLauncherSwitchingNode.cpp,tracktion_RackReturnNode.cpp,tracktion_RackNode.cpp- merge rulemodules/tracktion_engine/playback/graph/tracktion_EditNodeBuilder.cpp- gating increateNodeForAudioTrack/createLiveInputsNodemodules/tracktion_engine/model/edit/tracktion_Edit.{h,cpp}and/orAudioTrack- whatever property gates itTests
New, in the existing
TRACKTION_UNIT_TESTSblocks:tracktion_Node.test.cpp: aSummingNodewith one high-latency branch and one exempt branch - assert noLatencyNodeis wrapped around the exempt branch, the rootlatencyNumSamplesis unchanged, and the exempt branch's signal arrives at block 0 while the compensated branch stays aligned.tracktion_ConnectedNode.test.cpp: the same case through the multi-threaded player, sinceConnectedNodeduplicates the compensation logic and would otherwise diverge.LiveMidiInjectingNodeon the first - assert the injected note appears undelayed.Must stay green: the existing latency tests in
tracktion_Node.test.cpp,tracktion_NodeVisiting.test.cpp,tracktion_RackBenchmarks.test.cpp, and thedisableLatencyCompensationpath added in b5d5f9a.Edge cases
params.forRenderinghas to force it off, or renders become sample-inaccurate.TrackWaveInputDeviceNodederiveslatencyUpToThisPointfrom node properties to timestamp recorded material (tracktion_TrackWaveInputDeviceNode.cpp:45); changing branch latency changes punch-in offsets.RackNode/RackInstanceNode, includingautomationAdjustmentTimederived from latency.createLiveInputsNodebefore the track chain.MidiOutputDeviceInstanceInjectingNode) are a separate path and are not covered here.Edit::setLowLatencyMonitoring(which disables plugins) andEdit::setLatencyCompensationEnabled(Edit-wide off switch) needs defining - all three should not fight each other.Open questions
NodePropertiesbool, or the negativelatencyNumSamplessentinel from the issue comment? I'd recommend the bool for the reasons above, but it is your call.Fixes #209
🤖 Generated with Claude Code