diff --git a/modules/juce_audio_processors/processors/juce_AudioProcessorGraph.cpp b/modules/juce_audio_processors/processors/juce_AudioProcessorGraph.cpp index 316b9f3625..43a3608b1c 100644 --- a/modules/juce_audio_processors/processors/juce_AudioProcessorGraph.cpp +++ b/modules/juce_audio_processors/processors/juce_AudioProcessorGraph.cpp @@ -26,13 +26,157 @@ namespace juce { -void AudioProcessorGraph::NodeStates::setState (Optional newSettings) +/* Provides a comparison function for various types that have an associated NodeID, + for use with equal_range, lower_bound etc. +*/ +class ImplicitNode +{ +public: + using Node = AudioProcessorGraph::Node; + using NodeID = AudioProcessorGraph::NodeID; + using NodeAndChannel = AudioProcessorGraph::NodeAndChannel; + + ImplicitNode (NodeID x) : node (x) {} + ImplicitNode (NodeAndChannel x) : ImplicitNode (x.nodeID) {} + ImplicitNode (const Node* x) : ImplicitNode (x->nodeID) {} + ImplicitNode (const std::pair>& x) : ImplicitNode (x.first) {} + + /* This is the comparison function. */ + static bool compare (ImplicitNode a, ImplicitNode b) { return a.node < b.node; } + +private: + NodeID node; +}; + +//============================================================================== +/* A copyable type holding all the nodes, and allowing fast lookup by id. */ +class Nodes +{ +public: + using Node = AudioProcessorGraph::Node; + using NodeID = AudioProcessorGraph::NodeID; + + const ReferenceCountedArray& getNodes() const { return array; } + + Node::Ptr getNodeForId (NodeID x) const; + + Node::Ptr addNode (std::unique_ptr newProcessor, const NodeID nodeID); + Node::Ptr removeNode (NodeID nodeID); + + bool operator== (const Nodes& other) const { return array == other.array; } + bool operator!= (const Nodes& other) const { return array != other.array; } + +private: + ReferenceCountedArray array; +}; + +//============================================================================== +/* A value type holding a full set of graph connections. */ +class Connections +{ +public: + using Node = AudioProcessorGraph::Node; + using NodeID = AudioProcessorGraph::NodeID; + 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); + + 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; + + bool operator== (const Connections& other) const { return sourcesForDestination == other.sourcesForDestination; } + bool operator!= (const Connections& other) const { return sourcesForDestination != other.sourcesForDestination; } + +private: + using Map = std::map>; + + struct SearchState + { + std::set visited; + bool found = false; + }; + + SearchState getConnectedRecursive (NodeID source, NodeID dest, SearchState state) const; + static std::set removeIllegalConnections (const Nodes& n, + std::set sources, + NodeAndChannel destination); + std::pair getMatchingDestinations (NodeID destID) const; + + Map sourcesForDestination; +}; + +//============================================================================== +/* Settings used to prepare a node for playback. */ +struct PrepareSettings +{ + using ProcessingPrecision = AudioProcessorGraph::ProcessingPrecision; + + ProcessingPrecision precision = ProcessingPrecision::singlePrecision; + double sampleRate = 0.0; + int blockSize = 0; + + auto tie() const noexcept { return std::tie (precision, sampleRate, blockSize); } + + bool operator== (const PrepareSettings& other) const { return tie() == other.tie(); } + bool operator!= (const PrepareSettings& other) const { return tie() != other.tie(); } +}; + +//============================================================================== +/* Keeps track of the PrepareSettings applied to each node. */ +class NodeStates +{ +public: + using Node = AudioProcessorGraph::Node; + using NodeID = AudioProcessorGraph::NodeID; + + /* Called from prepareToPlay and releaseResources with the PrepareSettings that should be + used next time the graph is rebuilt. + */ + void setState (Optional newSettings); + + /* Call from the audio thread only. */ + Optional getLastRequestedSettings() const { return next; } + + /* Call from the main thread only! + + Called after updating the graph topology to prepare any currently-unprepared nodes. + + To ensure that all nodes are initialised with the same sample rate, buffer size, etc. as + the enclosing graph, we must ensure that any operation that uses these details (preparing + individual nodes) is synchronized with prepare-to-play and release-resources on the + enclosing graph. + + If the new PrepareSettings are different to the last-seen settings, all nodes will + be prepared/unprepared as necessary. If the PrepareSettings have not changed, then only + new nodes will be prepared/unprepared. + + 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 AudioProcessorGraph::NodeStates::applySettings (const Nodes& n) +Optional NodeStates::applySettings (const Nodes& n) { const auto settingsChanged = [this] { @@ -356,7 +500,7 @@ private: }; //============================================================================== -class AudioProcessorGraph::RenderSequenceBuilder +class RenderSequenceBuilder { public: using Node = AudioProcessorGraph::Node; @@ -868,7 +1012,13 @@ private: }; //============================================================================== -class AudioProcessorGraph::RenderSequence +/* A full graph of audio processors, ready to process at a particular sample rate, block size, + and precision. + + Instances of this class will be created on the main thread, and then passed over to the audio + thread for processing. +*/ +class RenderSequence { public: RenderSequence (PrepareSettings s, const Nodes& n, const Connections& c) @@ -888,12 +1038,12 @@ public: renderSequenceD.perform (audio, midi, playHead); } - void processIO (AudioGraphIOProcessor& io, AudioBuffer& audio, MidiBuffer& midi) + void processIO (AudioProcessorGraph::AudioGraphIOProcessor& io, AudioBuffer& audio, MidiBuffer& midi) { processIOBlock (io, renderSequenceF, audio, midi); } - void processIO (AudioGraphIOProcessor& io, AudioBuffer& audio, MidiBuffer& midi) + void processIO (AudioProcessorGraph::AudioGraphIOProcessor& io, AudioBuffer& audio, MidiBuffer& midi) { processIOBlock (io, renderSequenceD, audio, midi); } @@ -963,17 +1113,45 @@ private: }; //============================================================================== -AudioProcessorGraph::RenderSequenceExchange::RenderSequenceExchange() = default; -AudioProcessorGraph::RenderSequenceExchange::~RenderSequenceExchange() = default; +/* Facilitates wait-free render-sequence updates. -void AudioProcessorGraph::RenderSequenceExchange::set (std::unique_ptr&& next) + Topology updates always happen on the main thread (or synchronised with the main thread). + After updating the graph, the 'baked' graph is passed to RenderSequenceExchange::set. + At the top of the audio callback, RenderSequenceExchange::updateAudioThreadState will + attempt to install the most-recently-baked graph, if there's one waiting. +*/ +class RenderSequenceExchange +{ +public: + RenderSequenceExchange(); + ~RenderSequenceExchange(); + + void set (std::unique_ptr&&); + + /** Call from the audio thread only. */ + void updateAudioThreadState(); + + /** Call from the audio thread only. */ + RenderSequence* getAudioThreadState() const; + +private: + SpinLock mutex; + std::unique_ptr mainThreadState, audioThreadState; + 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 AudioProcessorGraph::RenderSequenceExchange::updateAudioThreadState() +void RenderSequenceExchange::updateAudioThreadState() { const SpinLock::ScopedTryLockType lock (mutex); @@ -985,20 +1163,20 @@ void AudioProcessorGraph::RenderSequenceExchange::updateAudioThreadState() } } -AudioProcessorGraph::RenderSequence* AudioProcessorGraph::RenderSequenceExchange::getAudioThreadState() const +RenderSequence* RenderSequenceExchange::getAudioThreadState() const { return audioThreadState.get(); } //============================================================================== -AudioProcessorGraph::Node::Ptr AudioProcessorGraph::Nodes::getNodeForId (NodeID nodeID) const +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 AudioProcessorGraph::Nodes::addNode (std::unique_ptr newProcessor, - const NodeID nodeID) +AudioProcessorGraph::Node::Ptr Nodes::addNode (std::unique_ptr newProcessor, + const NodeID nodeID) { if (newProcessor == nullptr) { @@ -1031,7 +1209,7 @@ AudioProcessorGraph::Node::Ptr AudioProcessorGraph::Nodes::addNode (std::unique_ new Node { nodeID, std::move (newProcessor) }); } -AudioProcessorGraph::Node::Ptr AudioProcessorGraph::Nodes::removeNode (NodeID nodeID) +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 @@ -1040,7 +1218,7 @@ AudioProcessorGraph::Node::Ptr AudioProcessorGraph::Nodes::removeNode (NodeID no } //============================================================================== -bool AudioProcessorGraph::Connections::addConnection (const Nodes& n, const Connection& c) +bool Connections::addConnection (const Nodes& n, const Connection& c) { if (! canConnect (n, c)) return false; @@ -1050,13 +1228,13 @@ bool AudioProcessorGraph::Connections::addConnection (const Nodes& n, const Conn return true; } -bool AudioProcessorGraph::Connections::removeConnection (const Connection& c) +bool Connections::removeConnection (const Connection& c) { const auto iter = sourcesForDestination.find (c.destination); return iter != sourcesForDestination.cend() && iter->second.erase (c.source) == 1; } -bool AudioProcessorGraph::Connections::removeIllegalConnections (const Nodes& n) +bool Connections::removeIllegalConnections (const Nodes& n) { auto anyRemoved = false; @@ -1070,7 +1248,7 @@ bool AudioProcessorGraph::Connections::removeIllegalConnections (const Nodes& n) return anyRemoved; } -bool AudioProcessorGraph::Connections::disconnectNode (NodeID n) +bool Connections::disconnectNode (NodeID n) { const auto matchingDestinations = getMatchingDestinations (n); auto result = matchingDestinations.first != matchingDestinations.second; @@ -1086,7 +1264,7 @@ bool AudioProcessorGraph::Connections::disconnectNode (NodeID n) return result; } -bool AudioProcessorGraph::Connections::isConnectionLegal (const Nodes& n, Connection c) +bool Connections::isConnectionLegal (const Nodes& n, Connection c) { const auto source = n.getNodeForId (c.source .nodeID); const auto dest = n.getNodeForId (c.destination.nodeID); @@ -1094,8 +1272,8 @@ bool AudioProcessorGraph::Connections::isConnectionLegal (const Nodes& n, Connec const auto sourceChannel = c.source .channelIndex; const auto destChannel = c.destination.channelIndex; - const auto sourceIsMIDI = midiChannelIndex == sourceChannel; - const auto destIsMIDI = midiChannelIndex == destChannel; + const auto sourceIsMIDI = AudioProcessorGraph::midiChannelIndex == sourceChannel; + const auto destIsMIDI = AudioProcessorGraph::midiChannelIndex == destChannel; return sourceChannel >= 0 && destChannel >= 0 @@ -1111,12 +1289,12 @@ bool AudioProcessorGraph::Connections::isConnectionLegal (const Nodes& n, Connec : destChannel < dest->getProcessor()->getTotalNumInputChannels()); } -bool AudioProcessorGraph::Connections::canConnect (const Nodes& n, Connection c) const +bool Connections::canConnect (const Nodes& n, Connection c) const { return isConnectionLegal (n, c) && ! isConnected (c); } -bool AudioProcessorGraph::Connections::isConnected (Connection c) const +bool Connections::isConnected (Connection c) const { const auto iter = sourcesForDestination.find (c.destination); @@ -1124,7 +1302,7 @@ bool AudioProcessorGraph::Connections::isConnected (Connection c) const && iter->second.find (c.source) != iter->second.cend(); } -bool AudioProcessorGraph::Connections::isConnected (NodeID srcID, NodeID destID) const +bool Connections::isConnected (NodeID srcID, NodeID destID) const { const auto matchingDestinations = getMatchingDestinations (destID); @@ -1135,7 +1313,7 @@ bool AudioProcessorGraph::Connections::isConnected (NodeID srcID, NodeID destID) }); } -std::set AudioProcessorGraph::Connections::getSourceNodesForDestination (NodeID destID) const +std::set Connections::getSourceNodesForDestination (NodeID destID) const { const auto matchingDestinations = getMatchingDestinations (destID); @@ -1148,13 +1326,13 @@ std::set AudioProcessorGraph::Connections::getSourc return result; } -std::set AudioProcessorGraph::Connections::getSourcesForDestination (const NodeAndChannel& p) const +std::set Connections::getSourcesForDestination (const NodeAndChannel& p) const { const auto iter = sourcesForDestination.find (p); return iter != sourcesForDestination.cend() ? iter->second : std::set{}; } -std::vector AudioProcessorGraph::Connections::getConnections() const +std::vector Connections::getConnections() const { std::vector result; @@ -1167,12 +1345,12 @@ std::vector AudioProcessorGraph::Connections::g return result; } -bool AudioProcessorGraph::Connections::isAnInputTo (NodeID source, NodeID dest) const +bool Connections::isAnInputTo (NodeID source, NodeID dest) const { return getConnectedRecursive (source, dest, {}).found; } -AudioProcessorGraph::Connections::SearchState AudioProcessorGraph::Connections::getConnectedRecursive (NodeID source, NodeID dest, SearchState state) const +Connections::SearchState Connections::getConnectedRecursive (NodeID source, NodeID dest, SearchState state) const { state.visited.insert (dest); @@ -1188,9 +1366,9 @@ AudioProcessorGraph::Connections::SearchState AudioProcessorGraph::Connections:: return state; } -std::set AudioProcessorGraph::Connections::removeIllegalConnections (const Nodes& n, - std::set sources, - NodeAndChannel destination) +std::set Connections::removeIllegalConnections (const Nodes& n, + std::set sources, + NodeAndChannel destination) { for (auto source = sources.cbegin(); source != sources.cend();) { @@ -1203,9 +1381,9 @@ std::set AudioProcessorGraph::Connections:: return sources; } -std::pair - AudioProcessorGraph::Connections::getMatchingDestinations (NodeID destID) const +std::pair + Connections::getMatchingDestinations (NodeID destID) const { return std::equal_range (sourcesForDestination.cbegin(), sourcesForDestination.cend(), destID, ImplicitNode::compare); } @@ -1240,75 +1418,281 @@ bool AudioProcessorGraph::Connection::operator< (const Connection& other) const } //============================================================================== -AudioProcessorGraph::AudioProcessorGraph() = default; - -AudioProcessorGraph::~AudioProcessorGraph() +class AudioProcessorGraph::Pimpl : private AsyncUpdater { - cancelPendingUpdate(); - clear(); -} +public: + explicit Pimpl (AudioProcessorGraph& o) : owner (&o) {} -const String AudioProcessorGraph::getName() const -{ - return "Audio Graph"; -} - -//============================================================================== -void AudioProcessorGraph::topologyChanged (UpdateKind updateKind) -{ - sendChangeMessage(); - - if (updateKind == UpdateKind::sync && MessageManager::getInstance()->isThisTheMessageThread()) - handleAsyncUpdate(); - else - triggerAsyncUpdate(); -} - -void AudioProcessorGraph::clear (UpdateKind updateKind) -{ - if (nodes.getNodes().isEmpty()) - return; - - nodes = Nodes{}; - connections = Connections{}; - topologyChanged (updateKind); -} - -AudioProcessorGraph::Node* AudioProcessorGraph::getNodeForId (NodeID nodeID) const -{ - return nodes.getNodeForId (nodeID); -} - -AudioProcessorGraph::Node::Ptr AudioProcessorGraph::addNode (std::unique_ptr newProcessor, const NodeID nodeID, UpdateKind updateKind) -{ - if (newProcessor.get() == this) + ~Pimpl() override { - jassertfalse; - return nullptr; + cancelPendingUpdate(); + clear (UpdateKind::sync); } - const auto idToUse = nodeID == NodeID() ? NodeID { ++(lastNodeID.uid) } : nodeID; + const auto& getNodes() const { return nodes.getNodes(); } - auto added = nodes.addNode (std::move (newProcessor), idToUse); + void clear (UpdateKind updateKind) + { + if (getNodes().isEmpty()) + return; - if (added == nullptr) - return nullptr; + nodes = Nodes{}; + connections = Connections{}; + topologyChanged (updateKind); + } - if (lastNodeID < idToUse) - lastNodeID = idToUse; + auto getNodeForId (NodeID nodeID) const + { + return nodes.getNodeForId (nodeID); + } - if (auto* ioProc = dynamic_cast (added->getProcessor())) - ioProc->setParentGraph (this); + Node::Ptr addNode (std::unique_ptr newProcessor, + const NodeID nodeID, + UpdateKind updateKind) + { + if (newProcessor.get() == owner) + { + jassertfalse; + return nullptr; + } - topologyChanged (updateKind); - return added; + const auto idToUse = nodeID == NodeID() ? NodeID { ++(lastNodeID.uid) } : nodeID; + + auto added = nodes.addNode (std::move (newProcessor), idToUse); + + if (added == nullptr) + return nullptr; + + if (lastNodeID < idToUse) + lastNodeID = idToUse; + + if (auto* ioProc = dynamic_cast (added->getProcessor())) + ioProc->setParentGraph (owner); + + topologyChanged (updateKind); + return added; + } + + Node::Ptr removeNode (NodeID nodeID, UpdateKind updateKind) + { + connections.disconnectNode (nodeID); + auto result = nodes.removeNode (nodeID); + topologyChanged (updateKind); + return result; + } + + std::vector getConnections() const + { + return connections.getConnections(); + } + + bool isConnected (const Connection& c) const + { + return connections.isConnected (c); + } + + bool isConnected (NodeID srcID, NodeID destID) const + { + return connections.isConnected (srcID, destID); + } + + bool isAnInputTo (Node& src, Node& dst) const + { + return connections.isAnInputTo (src.nodeID, dst.nodeID); + } + + bool canConnect (const Connection& c) const + { + return connections.canConnect (nodes, c); + } + + bool addConnection (const Connection& c, UpdateKind updateKind) + { + if (! connections.addConnection (nodes, c)) + return false; + + jassert (isConnected (c)); + topologyChanged (updateKind); + return true; + } + + bool removeConnection (const Connection& c, UpdateKind updateKind) + { + if (! connections.removeConnection (c)) + return false; + + topologyChanged (updateKind); + return true; + } + + bool disconnectNode (NodeID nodeID, UpdateKind updateKind) + { + if (! connections.disconnectNode (nodeID)) + return false; + + topologyChanged (updateKind); + return true; + } + + bool isConnectionLegal (const Connection& c) const + { + return connections.isConnectionLegal (nodes, c); + } + + bool removeIllegalConnections (UpdateKind updateKind) + { + const auto result = connections.removeIllegalConnections (nodes); + topologyChanged (updateKind); + return result; + } + + //============================================================================== + void prepareToPlay (double sampleRate, int estimatedSamplesPerBlock) + { + owner->setRateAndBufferSizeDetails (sampleRate, estimatedSamplesPerBlock); + + PrepareSettings settings; + settings.precision = owner->getProcessingPrecision(); + settings.sampleRate = sampleRate; + settings.blockSize = estimatedSamplesPerBlock; + + nodeStates.setState (settings); + + topologyChanged (UpdateKind::sync); + } + + void releaseResources() + { + nodeStates.setState (nullopt); + topologyChanged (UpdateKind::sync); + } + + void reset() + { + for (auto* n : getNodes()) + n->getProcessor()->reset(); + } + + void setNonRealtime (bool isProcessingNonRealtime) + { + for (auto* n : getNodes()) + n->getProcessor()->setNonRealtime (isProcessingNonRealtime); + } + + template + void processBlock (AudioBuffer& audio, MidiBuffer& midi, AudioPlayHead* playHead) + { + renderSequenceExchange.updateAudioThreadState(); + + if (renderSequenceExchange.getAudioThreadState() == nullptr && MessageManager::getInstance()->isThisTheMessageThread()) + handleAsyncUpdate(); + + if (owner->isNonRealtime()) + { + while (renderSequenceExchange.getAudioThreadState() == nullptr) + { + Thread::sleep (1); + renderSequenceExchange.updateAudioThreadState(); + } + } + + auto* state = renderSequenceExchange.getAudioThreadState(); + + // Only process if the graph has the correct blockSize, sampleRate etc. + if (state != nullptr && state->getSettings() == nodeStates.getLastRequestedSettings()) + { + state->process (audio, midi, playHead); + } + else + { + audio.clear(); + midi.clear(); + } + } + + /* Call from the audio thread only. */ + auto* getAudioThreadState() const { return renderSequenceExchange.getAudioThreadState(); } + +private: + void topologyChanged (UpdateKind updateKind) + { + owner->sendChangeMessage(); + + if (updateKind == UpdateKind::sync && MessageManager::getInstance()->isThisTheMessageThread()) + handleAsyncUpdate(); + else + triggerAsyncUpdate(); + } + + void handleAsyncUpdate() override + { + if (const auto newSettings = nodeStates.applySettings (nodes)) + { + auto sequence = std::make_unique (*newSettings, nodes, connections); + owner->setLatencySamples (sequence->getLatencySamples()); + renderSequenceExchange.set (std::move (sequence)); + } + else + { + renderSequenceExchange.set (nullptr); + } + } + + AudioProcessorGraph* owner = nullptr; + Nodes nodes; + Connections connections; + NodeStates nodeStates; + RenderSequenceExchange renderSequenceExchange; + NodeID lastNodeID; +}; + +//============================================================================== +AudioProcessorGraph::AudioProcessorGraph() : pimpl (std::make_unique (*this)) {} +AudioProcessorGraph::~AudioProcessorGraph() = default; + +const String AudioProcessorGraph::getName() const { return "Audio Graph"; } +bool AudioProcessorGraph::supportsDoublePrecisionProcessing() const { return true; } +double AudioProcessorGraph::getTailLengthSeconds() const { return 0; } +bool AudioProcessorGraph::acceptsMidi() const { return true; } +bool AudioProcessorGraph::producesMidi() const { return true; } +void AudioProcessorGraph::getStateInformation (MemoryBlock&) {} +void AudioProcessorGraph::setStateInformation (const void*, int) {} + +void AudioProcessorGraph::processBlock (AudioBuffer& audio, MidiBuffer& midi) { return pimpl->processBlock (audio, midi, getPlayHead()); } +void AudioProcessorGraph::processBlock (AudioBuffer& audio, MidiBuffer& midi) { return pimpl->processBlock (audio, midi, getPlayHead()); } +std::vector AudioProcessorGraph::getConnections() const { return pimpl->getConnections(); } +bool AudioProcessorGraph::addConnection (const Connection& c, UpdateKind updateKind) { return pimpl->addConnection (c, updateKind); } +bool AudioProcessorGraph::removeConnection (const Connection& c, UpdateKind updateKind) { return pimpl->removeConnection (c, updateKind); } +void AudioProcessorGraph::prepareToPlay (double sampleRate, int estimatedSamplesPerBlock) { return pimpl->prepareToPlay (sampleRate, estimatedSamplesPerBlock); } +void AudioProcessorGraph::clear (UpdateKind updateKind) { return pimpl->clear (updateKind); } +const ReferenceCountedArray& AudioProcessorGraph::getNodes() const noexcept { return pimpl->getNodes(); } +AudioProcessorGraph::Node* AudioProcessorGraph::getNodeForId (NodeID x) const { return pimpl->getNodeForId (x).get(); } +bool AudioProcessorGraph::disconnectNode (NodeID nodeID, UpdateKind updateKind) { return pimpl->disconnectNode (nodeID, updateKind); } +void AudioProcessorGraph::releaseResources() { return pimpl->releaseResources(); } +bool AudioProcessorGraph::removeIllegalConnections (UpdateKind updateKind) { return pimpl->removeIllegalConnections (updateKind); } +void AudioProcessorGraph::reset() { return pimpl->reset(); } +bool AudioProcessorGraph::canConnect (const Connection& c) const { return pimpl->canConnect (c); } +bool AudioProcessorGraph::isConnected (const Connection& c) const noexcept { return pimpl->isConnected (c); } +bool AudioProcessorGraph::isConnected (NodeID a, NodeID b) const noexcept { return pimpl->isConnected (a, b); } +bool AudioProcessorGraph::isConnectionLegal (const Connection& c) const { return pimpl->isConnectionLegal (c); } +bool AudioProcessorGraph::isAnInputTo (Node& source, Node& destination) const noexcept { return pimpl->isAnInputTo (source, destination); } + +AudioProcessorGraph::Node::Ptr AudioProcessorGraph::addNode (std::unique_ptr newProcessor, + NodeID nodeId, + UpdateKind updateKind) +{ + return pimpl->addNode (std::move (newProcessor), nodeId, updateKind); +} + +void AudioProcessorGraph::setNonRealtime (bool isProcessingNonRealtime) noexcept +{ + AudioProcessor::setNonRealtime (isProcessingNonRealtime); + pimpl->setNonRealtime (isProcessingNonRealtime); } AudioProcessorGraph::Node::Ptr AudioProcessorGraph::removeNode (NodeID nodeID, UpdateKind updateKind) { - auto result = nodes.removeNode (nodeID); - topologyChanged (updateKind); - return result; + return pimpl->removeNode (nodeID, updateKind); } AudioProcessorGraph::Node::Ptr AudioProcessorGraph::removeNode (Node* node, UpdateKind updateKind) @@ -1320,176 +1704,6 @@ AudioProcessorGraph::Node::Ptr AudioProcessorGraph::removeNode (Node* node, Upda return {}; } -//============================================================================== -std::vector AudioProcessorGraph::getConnections() const -{ - return connections.getConnections(); -} - -bool AudioProcessorGraph::isConnected (const Connection& c) const noexcept -{ - return connections.isConnected (c); -} - -bool AudioProcessorGraph::isConnected (NodeID srcID, NodeID destID) const noexcept -{ - return connections.isConnected (srcID, destID); -} - -bool AudioProcessorGraph::isAnInputTo (Node& src, Node& dst) const noexcept -{ - return connections.isAnInputTo (src.nodeID, dst.nodeID); -} - -bool AudioProcessorGraph::canConnect (const Connection& c) const -{ - return connections.canConnect (nodes, c); -} - -bool AudioProcessorGraph::addConnection (const Connection& c, UpdateKind updateKind) -{ - if (! connections.addConnection (nodes, c)) - return false; - - jassert (isConnected (c)); - topologyChanged (updateKind); - return true; -} - -bool AudioProcessorGraph::removeConnection (const Connection& c, UpdateKind updateKind) -{ - if (! connections.removeConnection (c)) - return false; - - topologyChanged (updateKind); - return true; -} - -bool AudioProcessorGraph::disconnectNode (NodeID nodeID, UpdateKind updateKind) -{ - if (! connections.disconnectNode (nodeID)) - return false; - - topologyChanged (updateKind); - return true; -} - -bool AudioProcessorGraph::isConnectionLegal (const Connection& c) const -{ - return connections.isConnectionLegal (nodes, c); -} - -bool AudioProcessorGraph::removeIllegalConnections (UpdateKind updateKind) -{ - auto result = connections.removeIllegalConnections (nodes); - topologyChanged (updateKind); - return result; -} - -//============================================================================== -void AudioProcessorGraph::handleAsyncUpdate() -{ - if (const auto newSettings = nodeStates.applySettings (nodes)) - { - auto sequence = std::make_unique (*newSettings, nodes, connections); - setLatencySamples (sequence->getLatencySamples()); - renderSequenceExchange.set (std::move (sequence)); - } - else - { - renderSequenceExchange.set (nullptr); - } -} - -//============================================================================== -void AudioProcessorGraph::prepareToPlay (double sampleRate, int estimatedSamplesPerBlock) -{ - setRateAndBufferSizeDetails (sampleRate, estimatedSamplesPerBlock); - - nodeStates.setState ([&] - { - PrepareSettings settings; - settings.precision = getProcessingPrecision(); - settings.sampleRate = sampleRate; - settings.blockSize = estimatedSamplesPerBlock; - return settings; - }()); - - topologyChanged (UpdateKind::sync); -} - -void AudioProcessorGraph::releaseResources() -{ - nodeStates.setState (nullopt); - topologyChanged (UpdateKind::sync); -} - -void AudioProcessorGraph::reset() -{ - for (auto* n : getNodes()) - n->getProcessor()->reset(); -} - -bool AudioProcessorGraph::supportsDoublePrecisionProcessing() const -{ - return true; -} - -void AudioProcessorGraph::setNonRealtime (bool isProcessingNonRealtime) noexcept -{ - AudioProcessor::setNonRealtime (isProcessingNonRealtime); - - for (auto* n : getNodes()) - n->getProcessor()->setNonRealtime (isProcessingNonRealtime); -} - -double AudioProcessorGraph::getTailLengthSeconds() const { return 0; } -bool AudioProcessorGraph::acceptsMidi() const { return true; } -bool AudioProcessorGraph::producesMidi() const { return true; } -void AudioProcessorGraph::getStateInformation (MemoryBlock&) {} -void AudioProcessorGraph::setStateInformation (const void*, int) {} - -template -void AudioProcessorGraph::processBlockImpl (AudioBuffer& audio, MidiBuffer& midi) -{ - renderSequenceExchange.updateAudioThreadState(); - - if (renderSequenceExchange.getAudioThreadState() == nullptr && MessageManager::getInstance()->isThisTheMessageThread()) - handleAsyncUpdate(); - - if (isNonRealtime()) - { - while (renderSequenceExchange.getAudioThreadState() == nullptr) - { - Thread::sleep (1); - renderSequenceExchange.updateAudioThreadState(); - } - } - - auto* state = renderSequenceExchange.getAudioThreadState(); - - // Only process if the graph has the correct blockSize, sampleRate etc. - if (state != nullptr && state->getSettings() == nodeStates.getLastRequestedSettings()) - { - state->process (audio, midi, playHead); - } - else - { - audio.clear(); - midi.clear(); - } -} - -void AudioProcessorGraph::processBlock (AudioBuffer& audio, MidiBuffer& midi) -{ - processBlockImpl (audio, midi); -} - -void AudioProcessorGraph::processBlock (AudioBuffer& audio, MidiBuffer& midi) -{ - processBlockImpl (audio, midi); -} - //============================================================================== AudioProcessorGraph::AudioGraphIOProcessor::AudioGraphIOProcessor (const IODeviceType deviceType) : type (deviceType) @@ -1552,7 +1766,7 @@ void AudioProcessorGraph::AudioGraphIOProcessor::processBlock (AudioBufferrenderSequenceExchange.getAudioThreadState()) + if (auto* state = graph->pimpl->getAudioThreadState()) state->processIO (*this, buffer, midiMessages); } @@ -1560,7 +1774,7 @@ void AudioProcessorGraph::AudioGraphIOProcessor::processBlock (AudioBufferrenderSequenceExchange.getAudioThreadState()) + if (auto* state = graph->pimpl->getAudioThreadState()) state->processIO (*this, buffer, midiMessages); } diff --git a/modules/juce_audio_processors/processors/juce_AudioProcessorGraph.h b/modules/juce_audio_processors/processors/juce_AudioProcessorGraph.h index f7c88f3c55..077eff1ac6 100644 --- a/modules/juce_audio_processors/processors/juce_AudioProcessorGraph.h +++ b/modules/juce_audio_processors/processors/juce_AudioProcessorGraph.h @@ -43,8 +43,7 @@ namespace juce @tags{Audio} */ class JUCE_API AudioProcessorGraph : public AudioProcessor, - public ChangeBroadcaster, - private AsyncUpdater + public ChangeBroadcaster { public: //============================================================================== @@ -222,7 +221,7 @@ public: void clear (UpdateKind = UpdateKind::sync); /** Returns the array of nodes in the graph. */ - const ReferenceCountedArray& getNodes() const noexcept { return nodes.getNodes(); } + const ReferenceCountedArray& getNodes() const noexcept; /** Returns the number of nodes in the graph. */ int getNumNodes() const noexcept { return getNodes().size(); } @@ -420,169 +419,8 @@ public: void setStateInformation (const void* data, int sizeInBytes) override; private: - class ImplicitNode - { - public: - // Implicit conversions for use with compareNodes in equal_range, lower_bound etc. - ImplicitNode (NodeID x) : node (x) {} - ImplicitNode (NodeAndChannel x) : ImplicitNode (x.nodeID) {} - ImplicitNode (const Node* x) : ImplicitNode (x->nodeID) {} - ImplicitNode (const std::pair>& x) : ImplicitNode (x.first) {} - - static bool compare (ImplicitNode a, ImplicitNode b) { return a.node < b.node; } - - private: - NodeID node; - }; - - /* A copyable type holding all the nodes, and allowing fast lookup by id. */ - class Nodes - { - public: - const ReferenceCountedArray& getNodes() const { return array; } - - Node::Ptr getNodeForId (NodeID x) const; - - Node::Ptr addNode (std::unique_ptr newProcessor, const NodeID nodeID); - Node::Ptr removeNode (NodeID nodeID); - - // If the arrays match, then the maps must also match. - bool operator== (const Nodes& other) const { return array == other.array; } - bool operator!= (const Nodes& other) const { return array != other.array; } - - private: - ReferenceCountedArray array; - }; - - /* A value type holding a full set of graph connections. */ - class Connections - { - public: - bool addConnection (const Nodes& n, const Connection& c); - bool removeConnection (const Connection& c); - bool removeIllegalConnections (const Nodes& n); - bool disconnectNode (NodeID n); - - 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 source, NodeID dest) const; - - bool operator== (const Connections& other) const { return sourcesForDestination == other.sourcesForDestination; } - bool operator!= (const Connections& other) const { return sourcesForDestination != other.sourcesForDestination; } - - private: - struct SearchState - { - std::set visited; - bool found = false; - }; - - using Map = std::map>; - - SearchState getConnectedRecursive (NodeID source, NodeID dest, SearchState state) const; - std::set removeIllegalConnections (const Nodes& n, std::set, NodeAndChannel); - std::pair getMatchingDestinations (NodeID destID) const; - - Map sourcesForDestination; - }; - - /* Settings used to prepare a node for playback. */ - struct PrepareSettings - { - ProcessingPrecision precision = ProcessingPrecision::singlePrecision; - double sampleRate = 0.0; - int blockSize = 0; - - auto tie() const noexcept { return std::tie (precision, sampleRate, blockSize); } - - bool operator== (const PrepareSettings& other) const { return tie() == other.tie(); } - bool operator!= (const PrepareSettings& other) const { return tie() != other.tie(); } - }; - - /* Keeps track of the PrepareSettings applied to each node. */ - class NodeStates - { - public: - /* Called from prepareToPlay and releaseResources with the PrepareSettings that should be - used next time the graph is rebuilt. - */ - void setState (Optional newSettings); - - /* Call from the audio thread only. - */ - Optional getLastRequestedSettings() const { return next; } - - /* Called after updating the graph topology (on the main thread) to prepare any currently- - unprepared nodes. - - To ensure that all nodes are initialised with the same sample rate, buffer size, etc. as - the enclosing graph, we must ensure that any operation that uses these details (preparing - individual nodes) is synchronized with prepare-to-play and release-resources on the - enclosing graph. - - If the new PrepareSettings are different to the last-seen settings, all nodes will - be prepared/unprepared as necessary. If the PrepareSettings have not changed, then only - new nodes will be prepared/unprepared. - - 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; - }; - - class RenderSequenceBuilder; - class RenderSequence; - - /* Facilitates wait-free render-sequence updates. - - Topology updates always happen on the main thread (or synchronised with the main thread). - After updating the graph, the 'baked' graph is passed to RenderSequenceExchange::set. - At the top of the audio callback, RenderSequenceExchange::get will return the render - sequence to use for that callback. - */ - class RenderSequenceExchange - { - public: - RenderSequenceExchange(); - ~RenderSequenceExchange(); - - void set (std::unique_ptr&&); - - /** Call from the audio thread only. */ - void updateAudioThreadState(); - - /** Call from the audio thread only. */ - RenderSequence* getAudioThreadState() const; - - private: - SpinLock mutex; - std::unique_ptr mainThreadState, audioThreadState; - bool isNew = false; - }; - - //============================================================================== - // These members represent the logical state of the graph, which can be queried and - // updated from the main thread. The actual playback state of the graph may lag behind - // this state a little. - Nodes nodes; - Connections connections; - NodeID lastNodeID = {}; - NodeStates nodeStates; - RenderSequenceExchange renderSequenceExchange; - - template - void processBlockImpl (AudioBuffer&, MidiBuffer&); - void topologyChanged (UpdateKind); - void handleAsyncUpdate() override; + class Pimpl; + std::unique_ptr pimpl; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioProcessorGraph) };