[BUG] Incorrect internal latency compensation in JUCE Audio Graph

Hey there,

I’ve encountered a latency compensation bug in the JUCE AudioProcessorGraph.

It happens when a node’s output channel is connected to multiple destination nodes with a larger delay, and the destination input channel indices are greater than the numOuts the respective destination nodes.

In this scenario, the audio graph does not add a copyChannelOp and applies latency compensation for each destination node. As addChannelDelayOp is an in-place op, this results in a delay applied twice to the second node, resulting in misaligned audio.

I’ve put together a simple reproducible example in this gist.

The graph is constructed as follows:

  • A ModStep node generates a step signal (value 0.4) between samples 30 and 60.
  • The ModStep node output channel 0 is connected to both Node1 (ch 1) and Node2 (ch 1).
  • Node0 introduces 100 samples of latency.

The signal path is:
Input → Node0 → Node1 → Node2 → Output
with the ModStep signal injected into Node1 and Node2 in parallel.

All the nodes (but ModStep) simply add all their input channels together in the first output channel.

The expected output is a step of amplitude 0.8 between sample 130 and 160 (30 to 60, + 100 latency) in the output buffer:

Index:      0   ---   129   130   ---   159   160   ---
Value:   0.00   ---  0.00  0.80   ---  0.80  0.00   ---

But instead, we get two copies of the original step signal, one at the expected position, and one with another 100 samples delay:

Index:      0   ---   129   130   ---   159   160   ---   229   230   ---   259   260   ---
Value:   0.00   ---  0.00  0.40   ---  0.40  0.00   ---  0.00  0.40   ---  0.40  0.00   ---

When different output channels are used, there’s no issue, and we get the expected behavior (use_same_out_channel_for_step = false in the example). Also, it works just fine when there’s no latency compensation to apply.

I don’t think I saw in the documentation that one should use different output channels for each destination node, so I believe this is a bug.

I modified the JUCE graph code on my end to add a copyChannelOp in this case (and mark the used buffer as assigned, even if inputChan >= numOuts), and it fixes the issue by doing some more copies (some could be avoided though).

But I think it should be fixed in the JUCE codebase itself. Let me know if you need more details or if I can help in any way.

1 Like

Can you post your JUCE fix here?

Sure, I’ll put my modifications in the original JUCE graph since I’m working on a custom version of it. I’ll try to do that this week.

I’ve added the fix to my gist. It consists in small modifications in createRenderingOpsForNode and findBufferForInputAudioChannel.

findBufferForInputAudioChannel now returns an additional bool, to indicate whether the returned buffer index should be marked as assigned even if inputChan >= numOuts. It also takes an additional argument (std::map<AudioProcessorGraph::NodeAndChannel, int> commonBuffer, so it’s able to reuse the same buffer (avoid copies) when the same source (NodeAndChannel) is connected to multiple inputs in the same node (with inIdx >= numOuts).

The fix does not handle the case where sources.size() > 1 (// Handle a mix of several outputs coming into this input.), as it’s never the case in my code. It’s left as an exercise! But I’m not even sure the bug can happen in this case.