| @@ -1046,12 +1046,13 @@ private: | |||
| enum { readOnlyEmptyBufferIndex = 0 }; | |||
| HashMap<uint32, int> delays; | |||
| std::unordered_map<uint32, int> 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<int> 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<AudioProcessorGraph::NodeID, AudioProcessor::BusesLayout>; | |||
| using NodeMap = std::map<AudioProcessorGraph::NodeID, NodeAttributes>; | |||
| 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<BasicProcessor> (layout, midiIn, midiOut); | |||
| } | |||
| static BusesProperties getInputOnlyProperties() | |||
| { | |||
| return BusesProperties().withInput ("in", AudioChannelSet::stereo()); | |||
| } | |||
| static BusesProperties getStereoProperties() | |||
| { | |||
| return BusesProperties().withInput ("in", AudioChannelSet::stereo()) | |||