| @@ -1046,12 +1046,13 @@ private: | |||||
| enum { readOnlyEmptyBufferIndex = 0 }; | enum { readOnlyEmptyBufferIndex = 0 }; | ||||
| HashMap<uint32, int> delays; | |||||
| std::unordered_map<uint32, int> delays; | |||||
| int totalLatency = 0; | int totalLatency = 0; | ||||
| int getNodeDelay (NodeID nodeID) const noexcept | 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 | int getInputLatencyForNode (const Connections& c, NodeID nodeID) const | ||||
| @@ -1379,7 +1380,7 @@ private: | |||||
| auto totalChans = jmax (numIns, numOuts); | auto totalChans = jmax (numIns, numOuts); | ||||
| Array<int> audioChannelsToUse; | Array<int> audioChannelsToUse; | ||||
| auto maxLatency = getInputLatencyForNode (c, node.nodeID); | |||||
| const auto maxInputLatency = getInputLatencyForNode (c, node.nodeID); | |||||
| for (int inputChan = 0; inputChan < numIns; ++inputChan) | for (int inputChan = 0; inputChan < numIns; ++inputChan) | ||||
| { | { | ||||
| @@ -1390,7 +1391,7 @@ private: | |||||
| node, | node, | ||||
| inputChan, | inputChan, | ||||
| ourRenderingIndex, | ourRenderingIndex, | ||||
| maxLatency); | |||||
| maxInputLatency); | |||||
| jassert (index >= 0); | jassert (index >= 0); | ||||
| audioChannelsToUse.add (index); | audioChannelsToUse.add (index); | ||||
| @@ -1413,10 +1414,11 @@ private: | |||||
| if (processor.producesMidi()) | if (processor.producesMidi()) | ||||
| midiBuffers.getReference (midiBufferToUse).channel = { node.nodeID, midiChannelIndex }; | 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) | if (numOuts == 0) | ||||
| totalLatency = maxLatency; | |||||
| totalLatency = jmax (totalLatency, thisNodeLatency); | |||||
| sequence.addProcessOp (node, audioChannelsToUse, totalChans, midiBufferToUse); | sequence.addProcessOp (node, audioChannelsToUse, totalChans, midiBufferToUse); | ||||
| } | } | ||||
| @@ -1548,6 +1550,25 @@ private: | |||||
| SequenceAndLatency sequence; | 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 | /* 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 | 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(); } | bool operator!= (const RenderSequenceSignature& other) const { return tie() != other.tie(); } | ||||
| private: | private: | ||||
| using NodeMap = std::map<AudioProcessorGraph::NodeID, AudioProcessor::BusesLayout>; | |||||
| using NodeMap = std::map<AudioProcessorGraph::NodeID, NodeAttributes>; | |||||
| static NodeMap getNodeMap (const Nodes& n) | static NodeMap getNodeMap (const Nodes& n) | ||||
| { | { | ||||
| @@ -1573,7 +1594,12 @@ private: | |||||
| NodeMap result; | NodeMap result; | ||||
| for (const auto& node : nodeRefs) | 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; | return result; | ||||
| } | } | ||||
| @@ -1912,7 +1938,6 @@ private: | |||||
| } | } | ||||
| } | } | ||||
| AudioProcessorGraph* owner = nullptr; | AudioProcessorGraph* owner = nullptr; | ||||
| Nodes nodes; | Nodes nodes; | ||||
| Connections connections; | 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"); | beginTest ("large render sequence can be built"); | ||||
| { | { | ||||
| AudioProcessorGraph graph; | AudioProcessorGraph graph; | ||||
| @@ -2275,6 +2336,11 @@ private: | |||||
| return std::make_unique<BasicProcessor> (layout, midiIn, midiOut); | return std::make_unique<BasicProcessor> (layout, midiIn, midiOut); | ||||
| } | } | ||||
| static BusesProperties getInputOnlyProperties() | |||||
| { | |||||
| return BusesProperties().withInput ("in", AudioChannelSet::stereo()); | |||||
| } | |||||
| static BusesProperties getStereoProperties() | static BusesProperties getStereoProperties() | ||||
| { | { | ||||
| return BusesProperties().withInput ("in", AudioChannelSet::stereo()) | return BusesProperties().withInput ("in", AudioChannelSet::stereo()) | ||||