haha - automatic delay compensation. Most DAWs have it, and it’s super difficult to get right. Have fun working on that as a free module
yeah, when you put it like that…
The difference I guess being that it attempts to compensate only for cable delays propagating through the module graph (not for, say, FFT performed by the module), but, yeah, a non-trivial problem to say the least!
EDIT: thought about this a bit (uh oh):
- Consider a scene in Rack as a digraph with modules as nodes, cables as edges, and inter-module delay as constant, positive edge weights (n=1). (Let’s assume we’re not using Little Utils Teleport or equivalent).
- This structure is highly amenable to well-understood graph algorithms. For example, the shortest weighted path between any two modules, if a path exists, can easily and efficiently be found with Dijkstra or a number of other simple graph algorithms, even when there are cycles (since the weights are always positive) and it won’t need recomputation unless there’s a cable operation (add, remove, repath).
One minimal, but useful, function, which I’d suggest as a proof-of-concept, is this:
- Allow the user to define a source module (A) and a final module (B).
- For each port on B, identify if there is a path from A to B that ends at the given port and determine its shortest weighted length. (In the graph formalism, we basically loop through the ports, temporarily delete all cables other than the one for the port, and then run Dijkstra or alternative).
- If there are two or more such ports, set port delays for each such port on B so that the sum of the path length plus the port delay for each connected port equals the longest path length minus one. So if there’s a direct connection from A to port B1, a two-hop connection from A to B2, and a five-hop connection from A to B3, set the additional delays as 4 on B1, 1 on B2, and 0 on B3.
To what end? Well, if A is an oscillator and B is a mixer, this would allow on-demand, automatic syncing up of parallel audio paths from a common source without comb filtering in cases where the parallel paths go through different numbers of modules. (Or imagine that A is a mid-side splitter and B is a mid-side combiner; I guess that’s just a specialized form of mixing, but now you don’t have to count modules in order to ensure that your different mid/side processing chains don’t get out of sync). Note that we’re not trying to eliminate or even minimize sync differences along the paths from A to B; we only care about it at the end.
The port delay adjustment be re-run manually, or possibly re-run automatically when there’s a cable change (since by definition the outcome can’t change unless there’s a cable change).
I think that would be a useful advanced feature. The adjustment algorithm could be added via module, but despite Rack’s flexibility I’m pretty sure that a module couldn’t add the per-port delays (maybe there’s some terrifying hack, but a custom build would seem to be the way to go here).
I don’t know how many more use cases there are here, or whether more advanced approaches would need to be used to deal with them. For me, the (probably impossible) hope has always been to reduce cable delay to zero EXCEPT when a single-sample delay is necessary to avoid feedback. But why? No one cares about start-to-end latency of a few samples; it’s [probably] only an issue when signals get out of sync and are then re-combined, and what I really like about @DaveVenom’s suggestion of a variable port delay is that you can compensate for that at the point of recombination without it changing the visible structure of your patch (and, if the tool-assisted graph theory trickery pans out, maybe even without manually counting and updating).
so yeah, that’s exactly what automatic delay compensation in a DAW does. It’s a good feature. The work is as you describe. Yes, it’s “basic” graph theory. Go for it! (btw, I think you need to break loops somewhat arbitrarily?)
Anyway, backing up, I think delay build into the port would be a cool feature. Thanks @DaveVenom .
One critical aspect that breaks graph/node processing latency compensation are feedback loops. Do you have any ideas on how that could be made to work?
For example: module A → B → C → A, which is not uncommon in modular setups. How do you decide which path takes priority? In feedback loops there is no possible way to properly compensate for the latency introduced by the loop, unless the API itself has some smart way to run some modules twice or something like that. I have not seen a single graph/node based API that has such functionality, if you know of any please do tell.
Some graph processing code even completely forbid the use of feedback loops, as it is the case of the JUCE AudioProcessorGraph class.
perhaps you didn’t notice I pointed out the loop issue in the previous comment.
ah yeah, you did not mention it by name so I missed it.
what “name” are you referring to?
feedback loop. that is the term I see always used for such things.
related discussion: AudioProcessorGraph with Feedback loops - General JUCE discussion - JUCE
Pedantically, in graph theory terms, I think we’re talking cycles, not just loops; a loop typically means an edge from a node to itself (like a self-patched module), but I think the issues are the same for @falkTX’s original A->B->C->A suggestion, which is a directed cycle (although in audio we’d typically call them both feedback loops, so…)
Anyway, given that cycles are the whole reason for the cable sample delay, we’d better be prepared to handle them!
My initial thought is this: we actually want what the basic algorithms will provide. Dijkstra’s algorithm doesn’t get caught in cycles–it finds the shortest weighted path, which by definition (for a graph with positive edge weights) can’t involve cycles. I think this is the behaviour we would typically desire for delay compensation.
Imagine this (I don’t have Rack on this machine or I’d work up a patch, so please forgive the text-heaviness):
S is our source oscillator. O is our output mixer. We want to time-align signals from S to O by different paths.
S goes straight to mixer channel 1 of O (dry path), but it also goes into a fancy feedbacking resonator that has three modules: A->B->C. B goes to channel 2 of O (wet path), but it also goes to C (probably an attenuator), which returns to A, forming the feedback loop/cycle.
I think that what we want is to align the straight path, S->O, with the direct path S->A->B->O. Pretend we generate a single-sample impulse from S. It reaches O with delay 1 via S->O; it reaches O with delay 3 via S->A->B->O; then via the cycle, it also reaches O with delay 6 via S->A->B->C->A->B->O, 9 via S->A->B->C->A->B->C->A->B->O, and so forth. The cycled signals simply can’t be time-aligned with each other (true in a physical system, too, even though the signals are propagating at much higher effective speeds!)
So we time-align using the first sample that escapes the cycle (having never gone through it), which, again, perfectly matches the shortest-path definition in a graph and is what Dijkstra et al. will give us. If the result of that algorithm doesn’t sound right, the next step would be to adjust the port delay manually.
Good news - I got a response with a decision about the enhancement request.
Bad news - the request was turned down
I am definitely disappointed, but at least the request did not disappear into the void - I appreciated getting an answer one way or the other.
Considering that in a typical patch maybe 1% or 2% of all input ports will use the delay feature, it looks like an overhead. When I need a delay, I use a dedicated module.
I am working on it with the Local Authorities. Call your Congressman and Senator.
Thanks for requesting and reporting back! I’m not surprised that @Vortico didn’t bite (in part due to @Ahornberg’s point, I’m guessing, and probably in part due to not wanting to make the GUI less explicit about what’s happening).
I’ll put the auto-syncing sample aligner I describe above onto my VCV ideas list for when I start coding in Rack again (November?) as there might be a (slightly clunky) way to squeeze it into a module after all.