AudioProcessorGraph and loops with addConnection(). What's really happening?

Good afternoon to everyone!
I recently made a post about me wanting to control loops presence in a AudioProcessorGraph.
Now, since I am not getting anywhere with my recent studies, I just tried to create connections that would generate loops and see what happens.

This is the situation: I have a Rack class, which contains the graph and istanciates a VCO and a SmartPan, which are two classes which inherit from a base class Module (which inherits from AudioProcessor). their bus layout is STEREOin - STEREOout.

After adding the nodes to the graph, as well as a AudioGraphIOProcessor node to attatch the outputs from the panner to the PluginProcessor, I started atting the stereo connections.

now I have this: VCO - SmartPan - audioOutputNode

Now, at this point, if I add a new connection that connects the SmartPan’s channels to the VCO, I don’t hear anything. Also, I am checking the RMSvalue of each processor’s buffer and in this state it seems like the SmartPan doesn’t get any data (RMSValue = 0) from the VCO. At first I assumed that the first connection between the SmartPan and the audioOutputNode would have gotten overwritten, but since there is no data inside the SmartPanner I believe the AudioProcessorGraph has detected the loop and stopped rendering that node.
Am I correct? How does loop detection works in AudioProcessorGraphs?
Especially without managing delay compensation, I would have thought some sort of crash was supposed to happen.

If the AudioProcessorGraph already checks for loops and acts as consequence, am I able to generate loops without anything bad (crashes, major glitches etc.) happening? If this is the case, I could just alert the user that he generated a loop and tell to remove the connection (on a PatchBay custom component), or implementing a simple undoManager.

Thank everyone in advance!

Giacomo

1 Like

Rail

IMO it would be better to avoid creating a loop - instead of creating a faulty
processor graph state including a loop and telling the user to undo.
I’d appreciate a way of querying the audio processor if connecting any two nodes
would create a loop - without actually having to create that connection.

1 Like

Yeah, that would be perfect.
I am trying to have a better understanding about all of this having a deep look at the AudioProcessorGraph.h/cpp.
It seems like the graph already manages latency, and also has a method called createOrderedNodeList(), which builds using an Assertion Sort algorythm. Still trying to see what happens with that list.

For now, if my plugin doesn’t explode after adding and connecting a node that generates loops, could it be helpful to set the node as bypassed(true) until I check if there’s a loop? This way the user wouldn’t hear crazy stuff until I set bypassed(false). Maybe a very short amount of time when I don’t hear anything?

And I could actually try to use that OrderedNodeList to check for loops. If a Node in position x, if I see the same parent Node before x for more than once, then I have a loop. Makes sense?

I need the loop check for my current project but it is way further down the task list so I have not investigated the problem fully.
Since the audio processor graph does not offer a loop check, my current plan is to simply implement the loop check externally.
So I will query the connection list from the graph and implement my own loop check on the connection graph - prior to changing anything in the graph itself.
This should be not too difficult since the check does not need to know any details of the nodes and since it only matters whether two nodes are connected at all, you need not take care of the connected channels.
I am optimistic I will manage to implement this from the outside - without looking into the implementation of the audio processor graph at all.
After all, the problem is a general graph-theoretic one and does not involve anything specific to the audio processor graph.
There are well-established algorithms for detecting circles in a directed graph and these are not difficult to implement.
(For example, there is an article on Baeldung on this topic).
But I think many people will need such a check, so an internal solution shipped with JUCE would be nice and will save me and others a lot of time if someone can come up with it.

1 Like

If it was possible to check for loops before connecting a Node, that would solve the problem! Maybe you could add the node without connecting it and get a list of possible connections, and see which ones generate a loop. The problem is that the bigger the graph (and the higher the amount of busses to connect), the higher the complexity of the algorithm is. mmh.

Agree!

Look at the cycle detection algorithm which I have linked (for a depth-first search method).
The complexity is linear in the total number of nodes and connections.
The algorithm assumes that nodes know their ingoing and outgoing connections, but in the AudioProcessorGraph, nodes are agnostic about their connections.
We can solve this by explicitly collecting the list of direct source nodes (from ingoing connections) and the list of direct destination nodes (from outgoing connections) for each node, and storing these associations externally.
This can be achieved in a single pass through the connection list.
The simulated new connection can then be injected simply by adding its source node to the source list associated with the destination node and by adding its destination node to the destination list associated with the source node.
After that, the algorithm can be applied “as is”.

1 Like

Perhaps canConnect will return false if a feedback loop would happen? (But I haven’t checked.)

https://docs.juce.com/master/classAudioProcessorGraph.html#a8c7e78a84e5a3e88f7ae2a475a753bfa

Looking into the code, canConnect() does not check for loops, except for the case of trivial loops where source and destination of a single connection coincide.

1 Like

That is true! So should we use the getConnections() method, in which each connection has its public attributes source and destination, which translate as ingoing and outgoing connections. I’m pretty sure that having two separate lists of sources and destinations would make it even easier to implement the algorithm. Nice! Will try soon and keep you guys updated.

Yes I would also try it that way.
If you store the ingoing nodes and out nodes in separate lists (instead of storing connections), you can easily add the tested simulated connection (without having to represent it as a connection object), simply by adding the involved nodes.

1 Like

Does anyone have any working code for detecting loops in Audioprocessorgraph? I need to do the same thing, but if other people have already written it, it can save a lot of time.

+1 for including it in the source code.

I think I posted some code here:

It’s recursive… so is slow in Debug but okay in Release.

Rail

Only thing I know is that since AudioProcessorGraphs are indeed graphs, you should be able to implement a topological sort algorithm, such as a DFS one (depth-first search). The thing is that in our case it’s kind of tricky to use having AudioProcessorGraphs, since every “getter” method you can call on them give you either pointers to Nodes (Node::Ptr) or NodeIDs. IDs should be easier to use, as long as you don’t need to know which AudioProcessor inherited object it’s associated with the NodeID (you get a pointer to AudioProcessor, so you don’t know what base class you use for the nodes, if you use one).

I just noticed that AudioProcessorGraph does have the function isAnInputTo(), which does a recursive search. So we should be able to do something like this:

if (!graph.isAnInputTo(newConnection.destination, newConnection.source)
    graph.addConnection(newConnection)

This should prevent loops in the audio graph before they occur, right?

This should prevent loops in the audio graph before they occur, right?

Well, this function is not enough, but it is for the core control you need to implement. The tricky part is feeding the function with the correct source and destination.
Also keep in mind that the function want Node& or NodeID as arguments, while newConnection.destination is a NodeAndChannel type. You can get the NodeID this way:

 graph.isAnInputTo(newConnection.destination.NodeID, newConnection.source.NodeID)

You’re right, I missed the .nodeID part. Can you explain why this function is not enough though? I have created a Connection object already with the correct source and destination information for the proposed connection. My reasoning is that if the destination is an input to the source, then connecting the source to a destination would create a loop, so we better not do it. Does this not cover all cases? (I’m not great at reasoning about this, so it’s quite possible that I’m overlooking something).

If you check manually for loops, and you know which source and destination to check, you’re all set. It’s better to implement an algorithm that checks for loops without caring how many Nodes and I/O busses they have each, and it can be (it will be) very inefficient to check every single connection possible.

In an automated search for loops you need to find a way to feed the function isAnInputTo() with all the nodes’ busses, using probably for cycles in which you pass the Nodes.