[FR] AudioProcessorGraph addConnections/removeConnections

Since AudioProcessorGraph is slow to buildRenderingSequence() due to it’s sort algorithm… I’m looking for a way to add and remove a batch of connections and only trigger buildRenderingSequence() at the end of the task.

I’ll probably write some code to add to my local copy of AudioProcessorGraph to create an OwnedArray of connections to add/remove and then trigger buildRenderingSequence() only after all the connections are changed…

Is this an addition you’d consider adding to the JUCE code?

My use-case is changing all my Mixer tracks’ outputs which means changing the connections for each track.

Thanks,

Rail

Yes, no objections to adding a simple optimisation like that. How many nodes does your graph have that makes it too slow?

Well a small drum kit - Kick In, Kick Out, Snare Top, Snare Bottom, Hat, 3 Toms, 6 Cymbals with limited bleed… OH & 1 stereo Room, 1 Mixer Aux Track:

There are 215 Nodes in the graph with 368 Connections

With something like the Metalllica live kit which has full bleed and has 2 Kicks (4 tracks), 1 Snare (2 Tracks), 1 Hat, 4 Toms and 6 Cymbals with only OH’s, but 8 Aux Tracks (in their Live preset):

There are 678 Nodes in the graph with 1,950 connections

This is with 32 Outputs and no additional effects instantiated in the Mixer.

I have reduced the number of processors as much as possible by combining them where possible.

The Aux Tracks add additional overhead because I have to recursively check for feedback in the connections to enable/disable Aux Sends on Aux Tracks.

If I could daisy chain/nest AudioProcessorGraphs I could reduce sort times by compartamentalizing graph changes … but I was never able to get that to work.

Thanks,

Rail

Ah, that is a pretty big graph!

Well it’s as small as I can make it while still having all the Bleed & Mixer functionality I want… and that’s without any Mixer built-in or external effects being used.

It’s what prompted my post: Can the AudioProcessorGraph please get some love and attention

Cheers,

Rail

Okay a report back… I made the changes and it’s dramatically improved the profiling of my plug-in… Since I rarely just make a single connection at a time… I added methods to use OwnedArray::add() instead of OwnedArray::addSorted() and defer buildRenderingSequence() like:

 bool AudioProcessorGraph::addConnectionUnsorted (const uint32 sourceNodeId,
                                                  const int sourceChannelIndex,
                                                  const uint32 destNodeId,
                                                  const int destChannelIndex)
 {
     const ScopedLock sl (buildLock);

     if (! canConnect (sourceNodeId, sourceChannelIndex, destNodeId, destChannelIndex))
         return false;

     connections.add (new Connection (sourceNodeId, sourceChannelIndex, destNodeId, destChannelIndex));

     return true;
 }

After I make all the connections I call another new method:

  void AudioProcessorGraph::sortConnections()
 {
     GraphRenderingOps::ConnectionSorter sorter;

     connections.sort (sorter);

     if (isPrepared)
         triggerAsyncUpdate();
 }

For other methods I added an extra parameter with a default value for backward compatibility:

   void AudioProcessorGraph::removeConnection (const int index, bool triggerUpdate /* = true */)
  {
      connections.remove (index);

      if (isPrepared && triggerUpdate)
          triggerAsyncUpdate();
  }

(I did this for addNode() and removeNode() as well).

This change has reduced the time to reload a preset, where the connections are created dynamically, by more than 50% and made other functions in the plug-in much more responsive if they involve changing connections.

My testers have had it for 2 weeks with no reports of any issues.


I’ve also added another change to AudioProcessorGraph::Node to have a boolean class variable (named bypassed) which is used to easily bypass hosted plugins in the graph simply by changing the Node’s flag which is then checked in (float and double versions)

void callProcess (AudioBuffer<float>& buffer, MidiBuffer& midiMessages)
{
    if (node->isBypassed())
        processor->processBlockBypassed (buffer, midiMessages);
    else
        processor->processBlock (buffer, midiMessages);
}

I also still have my previous changes for PDC described in this thread:

Cheers,

Rail

Just curious - did any element of these improvements make its way onto master?

1 Like