tracktion-engine 3.0-10-g034fdde4aa5
Tracktion Engine — High level data model for audio applications

« « « Anklang Documentation
Loading...
Searching...
No Matches
tracktion_Node.h
Go to the documentation of this file.
1 /*
2 ,--. ,--. ,--. ,--.
3 ,-' '-.,--.--.,--,--.,---.| |,-.,-' '-.`--' ,---. ,--,--, Copyright 2024
4 '-. .-'| .--' ,-. | .--'| /'-. .-',--.| .-. || \ Tracktion Software
5 | | | | \ '-' \ `--.| \ \ | | | |' '-' '| || | Corporation
6 `---' `--' `--`--'`---'`--'`--' `---' `--' `---' `--''--' www.tracktion.com
7
8 Tracktion Engine uses a GPL/commercial licence - see LICENCE.md for details.
9*/
10
11#pragma once
12
13//==============================================================================
14//==============================================================================
73namespace tracktion { inline namespace graph
74{
75
76class Node;
77
78//==============================================================================
79//==============================================================================
81template<typename NodeType, typename... Args>
86
87
88//==============================================================================
93{
94 choc::buffer::ChannelArrayView<float> view;
95 choc::buffer::ChannelArrayBuffer<float> data;
96};
97
98//==============================================================================
103{
104 Node* node = nullptr;
105 size_t id = 0;
106};
107
109inline bool operator< (NodeAndID n1, NodeAndID n2)
110{
111 return n1.id < n2.id;
112}
113
115inline bool operator== (NodeAndID n1, NodeAndID n2)
116{
117 return n1.node == n2.node && n1.id == n2.id;
118}
119
124{
125 std::unique_ptr<Node> rootNode;
126 std::vector<Node*> orderedNodes;
127 std::vector<NodeAndID> sortedNodes;
128};
129
130
133
134
135//==============================================================================
140{
141 double sampleRate;
142 int blockSize;
143 NodeGraph& nodeGraph;
144 NodeGraph* nodeGraphToReplace = nullptr;
145 std::function<NodeBuffer (choc::buffer::Size)> allocateAudioBuffer = nullptr;
146 std::function<void (NodeBuffer&&)> deallocateAudioBuffer = nullptr;
147 bool enableNodeMemorySharing = false; //** @internal */
148};
149
152{
153 bool hasAudio = false;
154 bool hasMidi = false;
155 int numberOfChannels = 0;
156 int latencyNumSamples = 0;
157 size_t nodeID = 0;
158};
159
160//==============================================================================
161//==============================================================================
164{
165 none,
168};
169
170enum class ClearBuffers
171{
172 no,
173 yes
174};
175
177{
178 no,
179 yes
180};
181
184{
185 ClearBuffers clear = ClearBuffers::yes;
186 AllocateAudioBuffer allocate = AllocateAudioBuffer::yes;
187};
188
189//==============================================================================
190//==============================================================================
192{
193public:
194 TransformCache() = default;
195
196 template<typename T>
197 void cacheProperty (size_t key, T value);
198
199 template<typename T>
200 T* getCachedProperty (size_t key);
201
202private:
204};
205
206//==============================================================================
207//==============================================================================
216class Node
217{
218public:
219 Node() = default;
220 virtual ~Node() = default;
221
222 //==============================================================================
225
227 void prepareForNextBlock (juce::Range<int64_t> referenceSampleRange);
228
239 void process (choc::buffer::FrameCount numSamples, juce::Range<int64_t> referenceSampleRange);
240
242 bool hasProcessed() const;
243
246 {
247 choc::buffer::ChannelArrayView<float> audio;
249 };
250
255
256 //==============================================================================
272 virtual TransformResult transform (Node& /*rootNode*/,
273 const std::vector<Node*>& /*postOrderedNodes*/,
275 {
276 return TransformResult::none;
277 }
278
281
289 virtual std::vector<Node*> getInternalNodes() { return {}; }
290
295
299 virtual bool isReadyToProcess() = 0;
300
303 {
304 choc::buffer::FrameCount numSamples;
305 juce::Range<int64_t> referenceSampleRange;
306 AudioAndMidiBuffer buffers;
307 };
308
309 //==============================================================================
314 void retain();
315
320 void release();
321
322 //==============================================================================
324 void* internal = nullptr;
325 int numOutputNodes = -1;
326 virtual size_t getAllocatedBytes() const;
327 void enablePreProcess (bool);
328
329protected:
337
341 virtual void prefetchBlock (juce::Range<int64_t> /*referenceSampleRange*/) {}
342
346 virtual void process (ProcessContext&) = 0;
347
352 virtual void preProcess (choc::buffer::FrameCount /*numSamples*/,
353 juce::Range<int64_t> /*referenceSampleRange*/)
354 {}
355
356 //==============================================================================
361
366 void setBufferViewToUse (Node* sourceNode, const choc::buffer::ChannelArrayView<float>&);
367
372 void setAudioOutput (Node* sourceNode, const choc::buffer::ChannelArrayView<float>&);
373
374private:
375 std::atomic<bool> hasBeenProcessed { false };
376 choc::buffer::Size audioBufferSize;
377 choc::buffer::ChannelArrayBuffer<float> audioBuffer;
378 choc::buffer::ChannelArrayView<float> audioView, allocatedView;
381 std::atomic<int> numSamplesProcessed { 0 }, retainCount { 0 };
382 NodeOptimisations nodeOptimisations;
383
384
385 std::vector<Node*> directInputNodes;
386 std::atomic<Node*> nodeToRelease { nullptr };
387 std::function<NodeBuffer (choc::buffer::Size)> allocateAudioBuffer = nullptr;
388 std::function<void (NodeBuffer&&)> deallocateAudioBuffer = nullptr;
389
390 #if JUCE_DEBUG
391 std::atomic<bool> isBeingProcessed { false };
392 #endif
393};
394
395//==============================================================================
396//==============================================================================
403template<typename Visitor>
404void visitNodes (Node&, Visitor&&, bool preordering);
405
406//==============================================================================
409{
410 preordering, // The order in which nodes are first visited
411 postordering, // The order in which nodes are last visited
412 reversePreordering, // The reverse of the preordering
413 reversePostordering, // The reverse of the postordering
414 bfsPreordering, // A breadth-first search
415 bfsReversePreordering // A reversed breadth-first search
416};
417
419static inline std::vector<Node*> getNodes (Node&, VertexOrdering);
420
421
422//==============================================================================
423//==============================================================================
430static inline std::vector<Node*> transformNodes (Node& rootNode)
431{
432 for (;;)
433 {
434 bool needToTransformAgain = false;
435
436 auto allNodes = getNodes (rootNode, VertexOrdering::postordering);
437 TransformCache cache;
438
439 for (auto node : allNodes)
440 {
441 const auto res = node->transform (rootNode, allNodes, cache);
442
443 if (res == TransformResult::none)
444 continue;
445
446 needToTransformAgain = true;
447
448 // Nodes may have been deleted from allNodes so start from the top
449 if (res == TransformResult::nodesDeleted)
450 break;
451
452 // New connections have been made but allNodes is still valid
453 assert (res == TransformResult::connectionsMade);
454 }
455
456 if (! needToTransformAgain)
457 return allNodes;
458 }
459}
460
461
462//==============================================================================
463//==============================================================================
465{
466 prepareToPlay (info);
467
468 auto props = getNodeProperties();
469 audioBufferSize = choc::buffer::Size::create ((choc::buffer::ChannelCount) props.numberOfChannels,
470 (choc::buffer::FrameCount) info.blockSize);
471
472 if (info.allocateAudioBuffer)
473 {
474 allocateAudioBuffer = info.allocateAudioBuffer;
475 deallocateAudioBuffer = info.deallocateAudioBuffer;
476 }
477 else if (nodeOptimisations.allocate == AllocateAudioBuffer::yes)
478 {
479 audioBuffer.resize (audioBufferSize);
480 }
481
482 directInputNodes = getDirectInputNodes();
483}
484
485inline void Node::prepareForNextBlock (juce::Range<int64_t> referenceSampleRange)
486{
487 // Only do this once as prepare may be called multiple times
488 if (retainCount == 0)
489 {
490 assert (directInputNodes.size() == getDirectInputNodes().size());
491 nodeToRelease.store (nullptr, std::memory_order_relaxed); // Reset in case the output node behaviour changes
492
493 retain();
494
495 for (auto& n : directInputNodes)
496 n->retain();
497 }
498
499 hasBeenProcessed.store (false, std::memory_order_release);
500 prefetchBlock (referenceSampleRange);
501}
502
503inline void Node::process (choc::buffer::FrameCount numSamples, juce::Range<int64_t> referenceSampleRange)
504{
505 #if JUCE_DEBUG
506 assert (! isBeingProcessed);
507 isBeingProcessed = true;
508
509 for (auto n : directInputNodes)
510 assert (n->hasProcessed());
511 #endif
512
513 preProcess (numSamples, referenceSampleRange);
514
515 // First, allocate buffers if possible
516 if (allocateAudioBuffer)
517 {
518 auto nodeBuffer = allocateAudioBuffer (audioBufferSize);
519 audioBuffer = std::move (nodeBuffer.data);
520 allocatedView = std::move (nodeBuffer.view);
521 assert (audioBufferSize == allocatedView.getSize());
522 }
523
524 if (nodeOptimisations.clear == ClearBuffers::yes)
525 {
526 audioBuffer.clear();
527 midiBuffer.clear();
528 }
529
530 const auto numChannelsBeforeProcessing = audioBuffer.getNumChannels();
531 const auto numSamplesBeforeProcessing = audioBuffer.getNumFrames();
532 juce::ignoreUnused (numChannelsBeforeProcessing, numSamplesBeforeProcessing);
533
534 jassert (numSamples > 0); // This must be a valid number of samples to process
535 jassert (numChannelsBeforeProcessing == 0 || numSamples <= audioBuffer.getNumFrames());
536
537 if (allocatedView.getSize() == audioBufferSize)
538 {
539 // Use a pre-allocated view if one has been initialised
540 audioView = allocatedView;
541 }
542 else
543 {
544 // Fallback to the internal buffer or an empty view
545 audioView = ((nodeOptimisations.allocate == AllocateAudioBuffer::yes ? audioBuffer.getView()
546 : referencedViewToUse ? referencedViewToUse->getFirstChannels (audioBufferSize.numChannels)
547 : choc::buffer::ChannelArrayView<float> { {}, audioBufferSize }));
548 }
549
550 audioView = audioView.getStart (numSamples);
551
552 auto destAudioView = audioView;
553 ProcessContext pc { numSamples, referenceSampleRange, { destAudioView, midiBuffer } };
554 process (pc);
555 numSamplesProcessed.store ((int) numSamples, std::memory_order_release);
556
557 jassert (numChannelsBeforeProcessing == audioBuffer.getNumChannels());
558 jassert (numSamplesBeforeProcessing == audioBuffer.getNumFrames());
559
560 release();
561
562 for (auto& n : directInputNodes)
563 n->release();
564
565 // If you've set a new view with setAudioOutput, they must be the same size!
566 jassert (destAudioView.getSize() == audioView.getSize());
567
568 #if JUCE_DEBUG
569 isBeingProcessed = false;
570 #endif
571
572 // N.B. This must be set last to release the Node back to the player
573 hasBeenProcessed.store (true, std::memory_order_release);
574}
575
576inline bool Node::hasProcessed() const
577{
578 return hasBeenProcessed.load (std::memory_order_acquire);
579}
580
582{
584
585 #if JUCE_DEBUG
586 if ([[ maybe_unused ]] auto node = nodeToRelease.load (std::memory_order_acquire))
587 jassert (node->hasProcessed());
588 #endif
589
590 return { audioView.getStart ((choc::buffer::FrameCount) numSamplesProcessed.load (std::memory_order_acquire)),
591 midiBuffer };
592}
593
594inline size_t Node::getAllocatedBytes() const
595{
596 return audioBuffer.getView().data.getBytesNeeded (audioBuffer.getSize())
597 + (size_t (midiBuffer.size()) * sizeof (tracktion_engine::MidiMessageArray::MidiMessageWithSource));
598}
599
600inline void Node::setOptimisations (NodeOptimisations newOptimisations)
601{
602 nodeOptimisations = newOptimisations;
603}
604
605inline void Node::setBufferViewToUse (Node* sourceNode, const choc::buffer::ChannelArrayView<float>& view)
606{
607 if (sourceNode)
608 {
609 sourceNode->retain();
610 nodeToRelease.store (sourceNode, std::memory_order_relaxed);
611 }
612
613 referencedViewToUse = view;
614}
615
616inline void Node::setAudioOutput (Node* sourceNode, const choc::buffer::ChannelArrayView<float>& newAudioView)
617{
618 if ([[ maybe_unused ]] auto node = nodeToRelease.load (std::memory_order_relaxed))
619 {
620 assert (sourceNode == node);
621 }
622 else if (sourceNode)
623 {
624 sourceNode->retain();
625 nodeToRelease.store (sourceNode, std::memory_order_relaxed);
626 }
627
628 audioView = newAudioView;
629}
630
631inline void Node::retain()
632{
633 assert (retainCount.load() >= 0);
634 retainCount.fetch_add (1, std::memory_order_relaxed);
635}
636
637inline void Node::release()
638{
639 assert (retainCount.load() > 0);
640
641 if (retainCount.fetch_sub (1, std::memory_order_acq_rel) == 1)
642 {
643 if (auto node = nodeToRelease.load (std::memory_order_relaxed))
644 node->release();
645
646 if (deallocateAudioBuffer)
647 deallocateAudioBuffer ({ std::move (allocatedView), std::move (audioBuffer) });
648 }
649}
650
651//==============================================================================
652//==============================================================================
653namespace detail
654{
656 {
657 template<typename Visitor>
658 static void visit (std::vector<Node*>& visitedNodes, Node& visitingNode, Visitor&& visitor, bool preordering)
659 {
660 if (std::find (visitedNodes.begin(), visitedNodes.end(), &visitingNode) != visitedNodes.end())
661 return;
662
663 if (preordering)
664 {
665 visitedNodes.push_back (&visitingNode);
666 visitor (visitingNode);
667 }
668
669 for (auto n : visitingNode.getDirectInputNodes())
670 visit (visitedNodes, *n, visitor, preordering);
671
672 if (! preordering)
673 {
674 visitedNodes.push_back (&visitingNode);
675 visitor (visitingNode);
676 }
677 }
678 };
679
681 {
682 template<typename Visitor>
683 static void visit (std::vector<Node*>& visitedNodes, Node& visitingNode, Visitor&& visitor)
684 {
685 if (std::find (visitedNodes.begin(), visitedNodes.end(), &visitingNode) == visitedNodes.end())
686 {
687 visitedNodes.push_back (&visitingNode);
688 visitor (visitingNode);
689 }
690
691 auto inputs = visitingNode.getDirectInputNodes();
692
693 // Visit each node then go back to the first and recurse
694 for (auto n : inputs)
695 {
696 if (std::find (visitedNodes.begin(), visitedNodes.end(), n) == visitedNodes.end())
697 {
698 visitedNodes.push_back (n);
699 visitor (visitingNode);
700 }
701 }
702
703 for (auto n : inputs)
704 visit (visitedNodes, *n, visitor);
705 }
706 };
707}
708
709template<typename Visitor>
710inline void visitNodes (Node& node, Visitor&& visitor, bool preordering)
711{
712 std::vector<Node*> visitedNodes;
713 detail::VisitNodesWithRecord::visit (visitedNodes, node, visitor, preordering);
714}
715
716template<typename Visitor>
717inline void visitNodesBFS (Node& node, Visitor&& visitor)
718{
719 std::vector<Node*> visitedNodes;
720 detail::VisitNodesWithRecordBFS::visit (visitedNodes, node, visitor);
721}
722
723inline std::vector<Node*> getNodes (Node& node, VertexOrdering vertexOrdering)
724{
725 if (vertexOrdering == VertexOrdering::bfsPreordering
726 || vertexOrdering == VertexOrdering::bfsReversePreordering)
727 {
728 std::vector<Node*> visitedNodes;
729 detail::VisitNodesWithRecordBFS::visit (visitedNodes, node, [](auto&){});
730
731 if (vertexOrdering == VertexOrdering::bfsReversePreordering)
732 std::reverse (visitedNodes.begin(), visitedNodes.end());
733
734 return visitedNodes;
735 }
736
737 bool preordering = vertexOrdering == VertexOrdering::preordering
738 || vertexOrdering == VertexOrdering::reversePreordering;
739
740 std::vector<Node*> visitedNodes;
741 detail::VisitNodesWithRecord::visit (visitedNodes, node, [](auto&){}, preordering);
742
743 if (vertexOrdering == VertexOrdering::reversePreordering
744 || vertexOrdering == VertexOrdering::reversePostordering)
745 std::reverse (visitedNodes.begin(), visitedNodes.end());
746
747 return visitedNodes;
748}
749
750inline void addNodesRecursive (std::vector<NodeAndID>& nodeMap, Node& n)
751{
752 nodeMap.push_back ({ &n, n.getNodeProperties().nodeID });
753
754 for (auto internalNode : n.getInternalNodes())
755 addNodesRecursive (nodeMap, *internalNode);
756}
757
758inline std::vector<NodeAndID> createNodeMap (const std::vector<Node*>& nodes)
759{
761
762 for (auto n : nodes)
763 addNodesRecursive (nodeMap, *n);
764
765 std::sort (nodeMap.begin(), nodeMap.end());
766 nodeMap.erase (std::unique (nodeMap.begin(), nodeMap.end()),
767 nodeMap.end());
768
769 return nodeMap;
770}
771
773{
774 assert (rootNode != nullptr);
775 auto orderedNodes = transformNodes (*rootNode);
776 auto sortedNodes = createNodeMap (orderedNodes);
777
778 // Iterate all nodes, for each input, increment the dest Node output count
779 for (auto node : orderedNodes)
780 node->numOutputNodes = 0;
781
782 for (auto node : orderedNodes)
783 for (auto inputNode : node->getDirectInputNodes())
784 ++inputNode->numOutputNodes;
785
786 auto nodeGraph = std::make_unique<NodeGraph>();
787 nodeGraph->rootNode = std::move (rootNode);
788 nodeGraph->orderedNodes = std::move (orderedNodes);
789 nodeGraph->sortedNodes = std::move (sortedNodes);
790
791 return nodeGraph;
792}
793
794template<typename T>
795inline void TransformCache::cacheProperty (size_t key, T value)
796{
797 cache[key] = std::move (value);
798}
799
800template<typename T>
801inline T* TransformCache::getCachedProperty (size_t key)
802{
803 if (auto found = cache.find (key); found != cache.end())
804 if (auto* typedValue = std::any_cast<T> (&found->second))
805 return typedValue;
806
807 return {};
808}
809
810}}
assert
T begin(T... args)
Main graph Node processor class.
virtual std::vector< Node * > getDirectInputNodes()
Should return all the inputs directly feeding in to this node.
virtual std::vector< Node * > getInternalNodes()
Can return Nodes that are internal to this Node but don't make up the main graph constructed from get...
virtual void prepareToPlay(const PlaybackInitialisationInfo &)
Called once before playback begins for each node.
void process(choc::buffer::FrameCount numSamples, juce::Range< int64_t > referenceSampleRange)
Call to process the node, which will in turn call the process method with the buffers to fill.
void setAudioOutput(Node *sourceNode, const choc::buffer::ChannelArrayView< float > &)
This can be called during your process function to set a view to the output.
virtual NodeProperties getNodeProperties()=0
Should return the properties of the node.
virtual bool isReadyToProcess()=0
Should return true when this node is ready to be processed.
void retain()
Retains the buffers so they won't be deallocated after the Node has processed.
virtual void process(ProcessContext &)=0
Called when the node is to be processed.
virtual void prefetchBlock(juce::Range< int64_t >)
Called before once on all Nodes before they are processed.
void prepareForNextBlock(juce::Range< int64_t > referenceSampleRange)
Call before processing the next block, used to reset the process status.
void setOptimisations(NodeOptimisations)
This can be called to provide some hints about allocating or playing back a Node to improve efficienc...
void setBufferViewToUse(Node *sourceNode, const choc::buffer::ChannelArrayView< float > &)
This can be called during prepareToPlay to set a BufferView to use which can improve efficiency.
bool hasProcessed() const
Returns true if this node has processed and its outputs can be retrieved.
void release()
Releases the buffers allowing internal storage to be deallocated.
void initialise(const PlaybackInitialisationInfo &)
Call once after the graph has been constructed to initialise buffers etc.
AudioAndMidiBuffer getProcessedOutput()
Returns the processed audio and MIDI output.
virtual TransformResult transform(Node &, const std::vector< Node * > &, TransformCache &)
Called after construction to give the node a chance to modify its topology.
virtual void preProcess(choc::buffer::FrameCount, juce::Range< int64_t >)
Called when the node is to be processed, just before process.
Contains the buffers for a processing operation.
Struct to describe a single iteration of a process call.
T end(T... args)
T erase(T... args)
T fetch_add(T... args)
T fetch_sub(T... args)
T find(T... args)
T is_pointer_v
#define jassert(expression)
T load(T... args)
void ignoreUnused(Types &&...) noexcept
void visitNodes(Node &, Visitor &&, bool preordering)
Should call the visitor for any direct inputs to the node exactly once.
VertexOrdering
Specifies the ordering algorithm.
std::unique_ptr< Node > makeNode(Args &&... args)
Creates a node of the given type and returns it as the base Node class.
TransformResult
Enum to signify the result of the transform function.
@ connectionsMade
No transform has been made.
@ nodesDeleted
New connections have been made.
std::unique_ptr< NodeGraph > createNodeGraph(std::unique_ptr< Node >)
Transforms a Node and then returns a NodeGraph of it ready to be initialised.
T push_back(T... args)
T reverse(T... args)
T sort(T... args)
T store(T... args)
A Node and its ID cached for quick lookup (without having to traverse the graph).
Holds a view over some data and optionally some storage for that data.
Holds a graph in an order ready for processing and a sorted map for quick lookups.
Holds some hints that might be used by the Node or players to improve efficiency.
Holds some really basic properties of a node.
Passed into Nodes when they are being initialised, to give them useful contextual information that th...
typedef size_t
T unique(T... args)