diff --git a/modules/juce_audio_processors/processors/juce_AudioProcessorGraph.cpp b/modules/juce_audio_processors/processors/juce_AudioProcessorGraph.cpp index 00c1e3990b..df2b1efcfb 100644 --- a/modules/juce_audio_processors/processors/juce_AudioProcessorGraph.cpp +++ b/modules/juce_audio_processors/processors/juce_AudioProcessorGraph.cpp @@ -1046,12 +1046,13 @@ private: enum { readOnlyEmptyBufferIndex = 0 }; - HashMap delays; + std::unordered_map delays; int totalLatency = 0; int getNodeDelay (NodeID nodeID) const noexcept { - return delays[nodeID.uid]; + const auto iter = delays.find (nodeID.uid); + return iter != delays.end() ? iter->second : 0; } int getInputLatencyForNode (const Connections& c, NodeID nodeID) const @@ -1379,7 +1380,7 @@ private: auto totalChans = jmax (numIns, numOuts); Array audioChannelsToUse; - auto maxLatency = getInputLatencyForNode (c, node.nodeID); + const auto maxInputLatency = getInputLatencyForNode (c, node.nodeID); for (int inputChan = 0; inputChan < numIns; ++inputChan) { @@ -1390,7 +1391,7 @@ private: node, inputChan, ourRenderingIndex, - maxLatency); + maxInputLatency); jassert (index >= 0); audioChannelsToUse.add (index); @@ -1413,10 +1414,11 @@ private: if (processor.producesMidi()) midiBuffers.getReference (midiBufferToUse).channel = { node.nodeID, midiChannelIndex }; - delays.set (node.nodeID.uid, maxLatency + processor.getLatencySamples()); + const auto thisNodeLatency = maxInputLatency + processor.getLatencySamples(); + delays[node.nodeID.uid] = thisNodeLatency; if (numOuts == 0) - totalLatency = maxLatency; + totalLatency = jmax (totalLatency, thisNodeLatency); sequence.addProcessOp (node, audioChannelsToUse, totalChans, midiBufferToUse); } @@ -1548,6 +1550,25 @@ private: SequenceAndLatency sequence; }; +//============================================================================== +/* Holds information about the properties of a graph node at the point it was prepared. + + If the bus layout or latency of a given node changes, the graph should be rebuilt so + that channel connections are ordered correctly, and the graph's internal delay lines have + the correct delay. +*/ +class NodeAttributes +{ + auto tie() const { return std::tie (layout, latencySamples); } + +public: + AudioProcessor::BusesLayout layout; + int latencySamples = 0; + + bool operator== (const NodeAttributes& other) const { return tie() == other.tie(); } + bool operator!= (const NodeAttributes& other) const { return tie() != other.tie(); } +}; + //============================================================================== /* Holds information about a particular graph configuration, without sharing ownership of any graph nodes. Can be checked for equality with other RenderSequenceSignature instances to see @@ -1565,7 +1586,7 @@ public: bool operator!= (const RenderSequenceSignature& other) const { return tie() != other.tie(); } private: - using NodeMap = std::map; + using NodeMap = std::map; static NodeMap getNodeMap (const Nodes& n) { @@ -1573,7 +1594,12 @@ private: NodeMap result; for (const auto& node : nodeRefs) - result.emplace (node->nodeID, node->getProcessor()->getBusesLayout()); + { + auto* proc = node->getProcessor(); + result.emplace (node->nodeID, + NodeAttributes { proc->getBusesLayout(), + proc->getLatencySamples() }); + } return result; } @@ -1912,7 +1938,6 @@ private: } } - AudioProcessorGraph* owner = nullptr; Nodes nodes; Connections connections; @@ -2204,6 +2229,42 @@ public: } } + beginTest ("rebuilding the graph recalculates overall latency"); + { + AudioProcessorGraph graph; + + const auto nodeA = graph.addNode (BasicProcessor::make (BasicProcessor::getStereoProperties(), MidiIn::no, MidiOut::no))->nodeID; + const auto nodeB = graph.addNode (BasicProcessor::make (BasicProcessor::getStereoProperties(), MidiIn::no, MidiOut::no))->nodeID; + const auto final = graph.addNode (BasicProcessor::make (BasicProcessor::getInputOnlyProperties(), MidiIn::no, MidiOut::no))->nodeID; + + expect (graph.addConnection ({ { nodeA, 0 }, { nodeB, 0 } })); + expect (graph.addConnection ({ { nodeA, 1 }, { nodeB, 1 } })); + expect (graph.addConnection ({ { nodeB, 0 }, { final, 0 } })); + expect (graph.addConnection ({ { nodeB, 1 }, { final, 1 } })); + + expect (graph.getLatencySamples() == 0); + + // Graph isn't built, latency is 0 if prepareToPlay hasn't been called yet + const auto nodeALatency = 100; + graph.getNodeForId (nodeA)->getProcessor()->setLatencySamples (nodeALatency); + graph.rebuild(); + expect (graph.getLatencySamples() == 0); + + graph.prepareToPlay (44100, 512); + + expect (graph.getLatencySamples() == nodeALatency); + + const auto nodeBLatency = 200; + graph.getNodeForId (nodeB)->getProcessor()->setLatencySamples (nodeBLatency); + graph.rebuild(); + expect (graph.getLatencySamples() == nodeALatency + nodeBLatency); + + const auto finalLatency = 300; + graph.getNodeForId (final)->getProcessor()->setLatencySamples (finalLatency); + graph.rebuild(); + expect (graph.getLatencySamples() == nodeALatency + nodeBLatency + finalLatency); + } + beginTest ("large render sequence can be built"); { AudioProcessorGraph graph; @@ -2275,6 +2336,11 @@ private: return std::make_unique (layout, midiIn, midiOut); } + static BusesProperties getInputOnlyProperties() + { + return BusesProperties().withInput ("in", AudioChannelSet::stereo()); + } + static BusesProperties getStereoProperties() { return BusesProperties().withInput ("in", AudioChannelSet::stereo())