|
|
@@ -58,10 +58,50 @@ public: |
|
|
|
|
|
|
|
const ReferenceCountedArray<Node>& 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<AudioProcessor> 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<AudioProcessor> 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<NodeID> getSourceNodesForDestination (NodeID destID) const;
|
|
|
|
std::set<NodeAndChannel> getSourcesForDestination (const NodeAndChannel& p) const;
|
|
|
|
std::vector<AudioProcessorGraph::Connection> 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<NodeID> getSourceNodesForDestination (NodeID destID) const
|
|
|
|
{
|
|
|
|
const auto matchingDestinations = getMatchingDestinations (destID);
|
|
|
|
|
|
|
|
std::set<NodeID> 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<NodeAndChannel> getSourcesForDestination (const NodeAndChannel& p) const
|
|
|
|
{
|
|
|
|
const auto iter = sourcesForDestination.find (p);
|
|
|
|
return iter != sourcesForDestination.cend() ? iter->second : std::set<NodeAndChannel>{};
|
|
|
|
}
|
|
|
|
|
|
|
|
std::vector<AudioProcessorGraph::Connection> getConnections() const
|
|
|
|
{
|
|
|
|
std::vector<Connection> 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<NodeAndChannel> 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<NodeAndChannel> removeIllegalConnections (const Nodes& nodes,
|
|
|
|
std::set<NodeAndChannel> sources,
|
|
|
|
NodeAndChannel destination);
|
|
|
|
std::pair<Map::const_iterator, Map::const_iterator> 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<Map::const_iterator, Map::const_iterator> 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<PrepareSettings> newSettings);
|
|
|
|
void setState (Optional<PrepareSettings> newSettings)
|
|
|
|
{
|
|
|
|
const std::lock_guard<std::mutex> lock (mutex);
|
|
|
|
next = newSettings;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Call from the audio thread only. */
|
|
|
|
Optional<PrepareSettings> getLastRequestedSettings() const { return next; }
|
|
|
@@ -162,74 +354,66 @@ public: |
|
|
|
|
|
|
|
Returns the settings that were applied to the nodes.
|
|
|
|
*/
|
|
|
|
Optional<PrepareSettings> applySettings (const Nodes& nodes);
|
|
|
|
|
|
|
|
private:
|
|
|
|
std::mutex mutex; // Protects 'next'
|
|
|
|
std::set<NodeID> preparedNodes;
|
|
|
|
Optional<PrepareSettings> current, next;
|
|
|
|
};
|
|
|
|
|
|
|
|
void NodeStates::setState (Optional<PrepareSettings> newSettings)
|
|
|
|
{
|
|
|
|
const std::lock_guard<std::mutex> lock (mutex);
|
|
|
|
next = newSettings;
|
|
|
|
}
|
|
|
|
|
|
|
|
Optional<PrepareSettings> NodeStates::applySettings (const Nodes& n)
|
|
|
|
{
|
|
|
|
const auto settingsChanged = [this]
|
|
|
|
{
|
|
|
|
const std::lock_guard<std::mutex> 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<PrepareSettings> applySettings (const Nodes& n)
|
|
|
|
{
|
|
|
|
for (const auto& node : n.getNodes())
|
|
|
|
node->getProcessor()->releaseResources();
|
|
|
|
const auto settingsChanged = [this]
|
|
|
|
{
|
|
|
|
const std::lock_guard<std::mutex> 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<NodeID> preparedNodes;
|
|
|
|
Optional<PrepareSettings> current, next;
|
|
|
|
};
|
|
|
|
|
|
|
|
//==============================================================================
|
|
|
|
template <typename FloatType>
|
|
|
@@ -1123,16 +1307,28 @@ private: |
|
|
|
class RenderSequenceExchange
|
|
|
|
{
|
|
|
|
public:
|
|
|
|
RenderSequenceExchange();
|
|
|
|
~RenderSequenceExchange();
|
|
|
|
|
|
|
|
void set (std::unique_ptr<RenderSequence>&&);
|
|
|
|
void set (std::unique_ptr<RenderSequence>&& 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<RenderSequence>&& 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<AudioProcessor> 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<AudioProcessorGraph::NodeID> Connections::getSourceNodesForDestination (NodeID destID) const
|
|
|
|
{
|
|
|
|
const auto matchingDestinations = getMatchingDestinations (destID);
|
|
|
|
|
|
|
|
std::set<NodeID> 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<AudioProcessorGraph::NodeAndChannel> Connections::getSourcesForDestination (const NodeAndChannel& p) const
|
|
|
|
{
|
|
|
|
const auto iter = sourcesForDestination.find (p);
|
|
|
|
return iter != sourcesForDestination.cend() ? iter->second : std::set<NodeAndChannel>{};
|
|
|
|
}
|
|
|
|
|
|
|
|
std::vector<AudioProcessorGraph::Connection> Connections::getConnections() const
|
|
|
|
{
|
|
|
|
std::vector<Connection> 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<AudioProcessorGraph::NodeAndChannel> Connections::removeIllegalConnections (const Nodes& n,
|
|
|
|
std::set<NodeAndChannel> 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::Map::const_iterator,
|
|
|
|
Connections::Map::const_iterator>
|
|
|
|
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)
|
|
|
|