diff --git a/modules/juce_audio_processors/processors/juce_AudioProcessorGraph.cpp b/modules/juce_audio_processors/processors/juce_AudioProcessorGraph.cpp index 43a3608b1c..4bfa1d76e5 100644 --- a/modules/juce_audio_processors/processors/juce_AudioProcessorGraph.cpp +++ b/modules/juce_audio_processors/processors/juce_AudioProcessorGraph.cpp @@ -58,10 +58,50 @@ public: const ReferenceCountedArray& getNodes() const { return array; } - Node::Ptr getNodeForId (NodeID x) const; + Node::Ptr getNodeForId (NodeID nodeID) const + { + const auto iter = std::lower_bound (array.begin(), array.end(), nodeID, ImplicitNode::compare); + return iter != array.end() && (*iter)->nodeID == nodeID ? *iter : nullptr; + } + + Node::Ptr addNode (std::unique_ptr newProcessor, const NodeID nodeID) + { + if (newProcessor == nullptr) + { + // Cannot add a null audio processor! + jassertfalse; + return {}; + } + + if (std::any_of (array.begin(), + array.end(), + [&] (auto* n) { return n->getProcessor() == newProcessor.get(); })) + { + // This audio processor has already been added to the graph! + jassertfalse; + return {}; + } + + const auto iter = std::lower_bound (array.begin(), array.end(), nodeID, ImplicitNode::compare); + + if (iter != array.end() && (*iter)->nodeID == nodeID) + { + // This nodeID has already been used for a node in the graph! + jassertfalse; + return {}; + } - Node::Ptr addNode (std::unique_ptr newProcessor, const NodeID nodeID); - Node::Ptr removeNode (NodeID nodeID); + return array.insert ((int) std::distance (array.begin(), iter), + new Node { nodeID, std::move (newProcessor) }); + } + + Node::Ptr removeNode (NodeID nodeID) + { + const auto iter = std::lower_bound (array.begin(), array.end(), nodeID, ImplicitNode::compare); + return iter != array.end() && (*iter)->nodeID == nodeID + ? array.removeAndReturn ((int) std::distance (array.begin(), iter)) + : nullptr; + } bool operator== (const Nodes& other) const { return array == other.array; } bool operator!= (const Nodes& other) const { return array != other.array; } @@ -80,19 +120,137 @@ public: using Connection = AudioProcessorGraph::Connection; using NodeAndChannel = AudioProcessorGraph::NodeAndChannel; - bool addConnection (const Nodes& n, const Connection& c); - bool removeConnection (const Connection& c); - bool removeIllegalConnections (const Nodes& n); - bool disconnectNode (NodeID n); + bool addConnection (const Nodes& n, const Connection& c) + { + if (! canConnect (n, c)) + return false; + + sourcesForDestination[c.destination].insert (c.source); + jassert (isConnected (c)); + return true; + } + + bool removeConnection (const Connection& c) + { + const auto iter = sourcesForDestination.find (c.destination); + return iter != sourcesForDestination.cend() && iter->second.erase (c.source) == 1; + } + + bool removeIllegalConnections (const Nodes& n) + { + auto anyRemoved = false; - static bool isConnectionLegal (const Nodes& n, Connection c); - bool canConnect (const Nodes& n, Connection c) const; - bool isConnected (Connection c) const; - bool isConnected (NodeID srcID, NodeID destID) const; - std::set getSourceNodesForDestination (NodeID destID) const; - std::set getSourcesForDestination (const NodeAndChannel& p) const; - std::vector getConnections() const; - bool isAnInputTo (NodeID src, NodeID dst) const; + for (auto& dest : sourcesForDestination) + { + const auto initialSize = dest.second.size(); + dest.second = removeIllegalConnections (n, std::move (dest.second), dest.first); + anyRemoved |= (dest.second.size() != initialSize); + } + + return anyRemoved; + } + + bool disconnectNode (NodeID n) + { + const auto matchingDestinations = getMatchingDestinations (n); + auto result = matchingDestinations.first != matchingDestinations.second; + sourcesForDestination.erase (matchingDestinations.first, matchingDestinations.second); + + for (auto& pair : sourcesForDestination) + { + const auto range = std::equal_range (pair.second.cbegin(), pair.second.cend(), n, ImplicitNode::compare); + result |= range.first != range.second; + pair.second.erase (range.first, range.second); + } + + return result; + } + + static bool isConnectionLegal (const Nodes& n, Connection c) + { + const auto source = n.getNodeForId (c.source .nodeID); + const auto dest = n.getNodeForId (c.destination.nodeID); + + const auto sourceChannel = c.source .channelIndex; + const auto destChannel = c.destination.channelIndex; + + const auto sourceIsMIDI = AudioProcessorGraph::midiChannelIndex == sourceChannel; + const auto destIsMIDI = AudioProcessorGraph::midiChannelIndex == destChannel; + + return sourceChannel >= 0 + && destChannel >= 0 + && source != dest + && sourceIsMIDI == destIsMIDI + && source != nullptr + && (sourceIsMIDI + ? source->getProcessor()->producesMidi() + : sourceChannel < source->getProcessor()->getTotalNumOutputChannels()) + && dest != nullptr + && (destIsMIDI + ? dest->getProcessor()->acceptsMidi() + : destChannel < dest->getProcessor()->getTotalNumInputChannels()); + } + + bool canConnect (const Nodes& n, Connection c) const + { + return isConnectionLegal (n, c) && ! isConnected (c); + } + + bool isConnected (Connection c) const + { + const auto iter = sourcesForDestination.find (c.destination); + + return iter != sourcesForDestination.cend() + && iter->second.find (c.source) != iter->second.cend(); + } + + bool isConnected (NodeID srcID, NodeID destID) const + { + const auto matchingDestinations = getMatchingDestinations (destID); + + return std::any_of (matchingDestinations.first, matchingDestinations.second, [srcID] (const auto& pair) + { + const auto iter = std::lower_bound (pair.second.cbegin(), pair.second.cend(), srcID, ImplicitNode::compare); + return iter != pair.second.cend() && iter->nodeID == srcID; + }); + } + + std::set getSourceNodesForDestination (NodeID destID) const + { + const auto matchingDestinations = getMatchingDestinations (destID); + + std::set result; + std::for_each (matchingDestinations.first, matchingDestinations.second, [&] (const auto& pair) + { + for (const auto& source : pair.second) + result.insert (source.nodeID); + }); + return result; + } + + std::set getSourcesForDestination (const NodeAndChannel& p) const + { + const auto iter = sourcesForDestination.find (p); + return iter != sourcesForDestination.cend() ? iter->second : std::set{}; + } + + std::vector getConnections() const + { + std::vector result; + + for (auto& pair : sourcesForDestination) + for (const auto& source : pair.second) + result.emplace_back (source, pair.first); + + std::sort (result.begin(), result.end()); + result.erase (std::unique (result.begin(), result.end()), result.end()); + return result; + } + + bool isAnInputTo (NodeID source, NodeID dest) const + { + return getConnectedRecursive (source, dest, {}).found; + } bool operator== (const Connections& other) const { return sourcesForDestination == other.sourcesForDestination; } bool operator!= (const Connections& other) const { return sourcesForDestination != other.sourcesForDestination; } @@ -106,11 +264,41 @@ private: bool found = false; }; - SearchState getConnectedRecursive (NodeID source, NodeID dest, SearchState state) const; - static std::set removeIllegalConnections (const Nodes& n, + SearchState getConnectedRecursive (NodeID source, NodeID dest, SearchState state) const + { + state.visited.insert (dest); + + for (const auto& s : getSourceNodesForDestination (dest)) + { + if (state.found || s == source) + return { std::move (state.visited), true }; + + if (state.visited.find (s) == state.visited.cend()) + state = getConnectedRecursive (source, s, std::move (state)); + } + + return state; + } + + static std::set removeIllegalConnections (const Nodes& nodes, std::set sources, - NodeAndChannel destination); - std::pair getMatchingDestinations (NodeID destID) const; + NodeAndChannel destination) + { + for (auto source = sources.cbegin(); source != sources.cend();) + { + if (! isConnectionLegal (nodes, { *source, destination })) + source = sources.erase (source); + else + ++source; + } + + return sources; + } + + std::pair getMatchingDestinations (NodeID destID) const + { + return std::equal_range (sourcesForDestination.cbegin(), sourcesForDestination.cend(), destID, ImplicitNode::compare); + } Map sourcesForDestination; }; @@ -142,7 +330,11 @@ public: /* Called from prepareToPlay and releaseResources with the PrepareSettings that should be used next time the graph is rebuilt. */ - void setState (Optional newSettings); + void setState (Optional newSettings) + { + const std::lock_guard lock (mutex); + next = newSettings; + } /* Call from the audio thread only. */ Optional getLastRequestedSettings() const { return next; } @@ -162,74 +354,66 @@ public: Returns the settings that were applied to the nodes. */ - Optional applySettings (const Nodes& nodes); - -private: - std::mutex mutex; // Protects 'next' - std::set preparedNodes; - Optional current, next; -}; - -void NodeStates::setState (Optional newSettings) -{ - const std::lock_guard lock (mutex); - next = newSettings; -} - -Optional NodeStates::applySettings (const Nodes& n) -{ - const auto settingsChanged = [this] - { - const std::lock_guard lock (mutex); - const auto result = current != next; - current = next; - return result; - }(); - - // It may look like releaseResources and prepareToPlay could race with calls to processBlock - // here, because applySettings is called from the main thread, processBlock is called from - // the audio thread (normally), and there's no explicit mutex ensuring that the calls don't - // overlap. - // However, it is part of the AudioProcessor contract that users shall not call - // processBlock, prepareToPlay, and/or releaseResources concurrently. That is, there's an - // implied mutex synchronising these functions on each AudioProcessor. - // - // Inside processBlock, we always ensure that the current RenderSequence's PrepareSettings - // match the graph's settings before attempting to call processBlock on any of the graph - // nodes; as a result, it's impossible to start calling processBlock on a node on the audio - // thread while a render sequence rebuild (including prepareToPlay/releaseResources calls) - // is already in progress here. - // - // Due to the implied mutex between prepareToPlay/releaseResources/processBlock, it's also - // impossible to receive new PrepareSettings and to start a new RenderSequence rebuild while - // a processBlock call is in progress. - - if (settingsChanged) + Optional applySettings (const Nodes& n) { - for (const auto& node : n.getNodes()) - node->getProcessor()->releaseResources(); + const auto settingsChanged = [this] + { + const std::lock_guard lock (mutex); + const auto result = current != next; + current = next; + return result; + }(); + + // It may look like releaseResources and prepareToPlay could race with calls to processBlock + // here, because applySettings is called from the main thread, processBlock is called from + // the audio thread (normally), and there's no explicit mutex ensuring that the calls don't + // overlap. + // However, it is part of the AudioProcessor contract that users shall not call + // processBlock, prepareToPlay, and/or releaseResources concurrently. That is, there's an + // implied mutex synchronising these functions on each AudioProcessor. + // + // Inside processBlock, we always ensure that the current RenderSequence's PrepareSettings + // match the graph's settings before attempting to call processBlock on any of the graph + // nodes; as a result, it's impossible to start calling processBlock on a node on the audio + // thread while a render sequence rebuild (including prepareToPlay/releaseResources calls) + // is already in progress here. + // + // Due to the implied mutex between prepareToPlay/releaseResources/processBlock, it's also + // impossible to receive new PrepareSettings and to start a new RenderSequence rebuild while + // a processBlock call is in progress. + + if (settingsChanged) + { + for (const auto& node : n.getNodes()) + node->getProcessor()->releaseResources(); - preparedNodes.clear(); - } + preparedNodes.clear(); + } - if (current.hasValue()) - { - for (const auto& node : n.getNodes()) + if (current.hasValue()) { - if (preparedNodes.find (node->nodeID) != preparedNodes.cend()) - continue; + for (const auto& node : n.getNodes()) + { + if (preparedNodes.find (node->nodeID) != preparedNodes.cend()) + continue; - preparedNodes.insert (node->nodeID); + preparedNodes.insert (node->nodeID); - node->getProcessor()->setProcessingPrecision (node->getProcessor()->supportsDoublePrecisionProcessing() ? current->precision - : AudioProcessor::singlePrecision); - node->getProcessor()->setRateAndBufferSizeDetails (current->sampleRate, current->blockSize); - node->getProcessor()->prepareToPlay (current->sampleRate, current->blockSize); + node->getProcessor()->setProcessingPrecision (node->getProcessor()->supportsDoublePrecisionProcessing() ? current->precision + : AudioProcessor::singlePrecision); + node->getProcessor()->setRateAndBufferSizeDetails (current->sampleRate, current->blockSize); + node->getProcessor()->prepareToPlay (current->sampleRate, current->blockSize); + } } + + return current; } - return current; -} +private: + std::mutex mutex; + std::set preparedNodes; + Optional current, next; +}; //============================================================================== template @@ -1123,16 +1307,28 @@ private: class RenderSequenceExchange { public: - RenderSequenceExchange(); - ~RenderSequenceExchange(); - - void set (std::unique_ptr&&); + void set (std::unique_ptr&& next) + { + const SpinLock::ScopedLockType lock (mutex); + mainThreadState = std::move (next); + isNew = true; + } /** Call from the audio thread only. */ - void updateAudioThreadState(); + void updateAudioThreadState() + { + const SpinLock::ScopedTryLockType lock (mutex); + + if (lock.isLocked() && isNew) + { + // Swap pointers rather than assigning to avoid calling delete here + std::swap (mainThreadState, audioThreadState); + isNew = false; + } + } /** Call from the audio thread only. */ - RenderSequence* getAudioThreadState() const; + RenderSequence* getAudioThreadState() const { return audioThreadState.get(); } private: SpinLock mutex; @@ -1140,255 +1336,6 @@ private: bool isNew = false; }; -//============================================================================== -RenderSequenceExchange::RenderSequenceExchange() = default; -RenderSequenceExchange::~RenderSequenceExchange() = default; - -void RenderSequenceExchange::set (std::unique_ptr&& next) -{ - const SpinLock::ScopedLockType lock (mutex); - mainThreadState = std::move (next); - isNew = true; -} - -void RenderSequenceExchange::updateAudioThreadState() -{ - const SpinLock::ScopedTryLockType lock (mutex); - - if (lock.isLocked() && isNew) - { - // Swap pointers rather than assigning to avoid calling delete here - std::swap (mainThreadState, audioThreadState); - isNew = false; - } -} - -RenderSequence* RenderSequenceExchange::getAudioThreadState() const -{ - return audioThreadState.get(); -} - -//============================================================================== -AudioProcessorGraph::Node::Ptr Nodes::getNodeForId (NodeID nodeID) const -{ - const auto iter = std::lower_bound (array.begin(), array.end(), nodeID, ImplicitNode::compare); - return iter != array.end() && (*iter)->nodeID == nodeID ? *iter : nullptr; -} - -AudioProcessorGraph::Node::Ptr Nodes::addNode (std::unique_ptr newProcessor, - const NodeID nodeID) -{ - if (newProcessor == nullptr) - { - // Cannot add a null audio processor! - jassertfalse; - return {}; - } - - if (std::any_of (array.begin(), - array.end(), - [&] (auto* n) { return n->getProcessor() == newProcessor.get(); })) - { - // This audio processor has already been added to the graph! - jassertfalse; - return {}; - - - } - - const auto iter = std::lower_bound (array.begin(), array.end(), nodeID, ImplicitNode::compare); - - if (iter != array.end() && (*iter)->nodeID == nodeID) - { - // This nodeID has already been used for a node in the graph! - jassertfalse; - return {}; - } - - return array.insert ((int) std::distance (array.begin(), iter), - new Node { nodeID, std::move (newProcessor) }); -} - -AudioProcessorGraph::Node::Ptr Nodes::removeNode (NodeID nodeID) -{ - const auto iter = std::lower_bound (array.begin(), array.end(), nodeID, ImplicitNode::compare); - return iter != array.end() && (*iter)->nodeID == nodeID - ? array.removeAndReturn ((int) std::distance (array.begin(), iter)) - : nullptr; -} - -//============================================================================== -bool Connections::addConnection (const Nodes& n, const Connection& c) -{ - if (! canConnect (n, c)) - return false; - - sourcesForDestination[c.destination].insert (c.source); - jassert (isConnected (c)); - return true; -} - -bool Connections::removeConnection (const Connection& c) -{ - const auto iter = sourcesForDestination.find (c.destination); - return iter != sourcesForDestination.cend() && iter->second.erase (c.source) == 1; -} - -bool Connections::removeIllegalConnections (const Nodes& n) -{ - auto anyRemoved = false; - - for (auto& dest : sourcesForDestination) - { - const auto initialSize = dest.second.size(); - dest.second = removeIllegalConnections (n, std::move (dest.second), dest.first); - anyRemoved |= (dest.second.size() != initialSize); - } - - return anyRemoved; -} - -bool Connections::disconnectNode (NodeID n) -{ - const auto matchingDestinations = getMatchingDestinations (n); - auto result = matchingDestinations.first != matchingDestinations.second; - sourcesForDestination.erase (matchingDestinations.first, matchingDestinations.second); - - for (auto& pair : sourcesForDestination) - { - const auto range = std::equal_range (pair.second.cbegin(), pair.second.cend(), n, ImplicitNode::compare); - result |= range.first != range.second; - pair.second.erase (range.first, range.second); - } - - return result; -} - -bool Connections::isConnectionLegal (const Nodes& n, Connection c) -{ - const auto source = n.getNodeForId (c.source .nodeID); - const auto dest = n.getNodeForId (c.destination.nodeID); - - const auto sourceChannel = c.source .channelIndex; - const auto destChannel = c.destination.channelIndex; - - const auto sourceIsMIDI = AudioProcessorGraph::midiChannelIndex == sourceChannel; - const auto destIsMIDI = AudioProcessorGraph::midiChannelIndex == destChannel; - - return sourceChannel >= 0 - && destChannel >= 0 - && source != dest - && sourceIsMIDI == destIsMIDI - && source != nullptr - && (sourceIsMIDI - ? source->getProcessor()->producesMidi() - : sourceChannel < source->getProcessor()->getTotalNumOutputChannels()) - && dest != nullptr - && (destIsMIDI - ? dest->getProcessor()->acceptsMidi() - : destChannel < dest->getProcessor()->getTotalNumInputChannels()); -} - -bool Connections::canConnect (const Nodes& n, Connection c) const -{ - return isConnectionLegal (n, c) && ! isConnected (c); -} - -bool Connections::isConnected (Connection c) const -{ - const auto iter = sourcesForDestination.find (c.destination); - - return iter != sourcesForDestination.cend() - && iter->second.find (c.source) != iter->second.cend(); -} - -bool Connections::isConnected (NodeID srcID, NodeID destID) const -{ - const auto matchingDestinations = getMatchingDestinations (destID); - - return std::any_of (matchingDestinations.first, matchingDestinations.second, [srcID] (const auto& pair) - { - const auto iter = std::lower_bound (pair.second.cbegin(), pair.second.cend(), srcID, ImplicitNode::compare); - return iter != pair.second.cend() && iter->nodeID == srcID; - }); -} - -std::set Connections::getSourceNodesForDestination (NodeID destID) const -{ - const auto matchingDestinations = getMatchingDestinations (destID); - - std::set result; - std::for_each (matchingDestinations.first, matchingDestinations.second, [&] (const auto& pair) - { - for (const auto& source : pair.second) - result.insert (source.nodeID); - }); - return result; -} - -std::set Connections::getSourcesForDestination (const NodeAndChannel& p) const -{ - const auto iter = sourcesForDestination.find (p); - return iter != sourcesForDestination.cend() ? iter->second : std::set{}; -} - -std::vector Connections::getConnections() const -{ - std::vector result; - - for (auto& pair : sourcesForDestination) - for (const auto& source : pair.second) - result.emplace_back (source, pair.first); - - std::sort (result.begin(), result.end()); - result.erase (std::unique (result.begin(), result.end()), result.end()); - return result; -} - -bool Connections::isAnInputTo (NodeID source, NodeID dest) const -{ - return getConnectedRecursive (source, dest, {}).found; -} - -Connections::SearchState Connections::getConnectedRecursive (NodeID source, NodeID dest, SearchState state) const -{ - state.visited.insert (dest); - - for (const auto& s : getSourceNodesForDestination (dest)) - { - if (state.found || s == source) - return { std::move (state.visited), true }; - - if (state.visited.find (s) == state.visited.cend()) - state = getConnectedRecursive (source, s, std::move (state)); - } - - return state; -} - -std::set Connections::removeIllegalConnections (const Nodes& n, - std::set sources, - NodeAndChannel destination) -{ - for (auto source = sources.cbegin(); source != sources.cend();) - { - if (! isConnectionLegal (n, { *source, destination })) - source = sources.erase (source); - else - ++source; - } - - return sources; -} - -std::pair - Connections::getMatchingDestinations (NodeID destID) const -{ - return std::equal_range (sourcesForDestination.cbegin(), sourcesForDestination.cend(), destID, ImplicitNode::compare); -} - -//============================================================================== //============================================================================== AudioProcessorGraph::Connection::Connection (NodeAndChannel src, NodeAndChannel dst) noexcept : source (src), destination (dst)