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

« « « Anklang Documentation
Loading...
Searching...
No Matches
tracktion_ArrangerLauncherSwitchingNode.cpp
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
12
13namespace tracktion { inline namespace engine
14{
15
16//==============================================================================
17//==============================================================================
18ArrangerLauncherSwitchingNode::ArrangerLauncherSwitchingNode (ProcessState& ps,
19 AudioTrack& at,
20 std::unique_ptr<Node> arrangerNode_,
22 : TracktionEngineNode (ps),
23 track (at),
24 arrangerNode (std::move (arrangerNode_)),
25 launcherNodes (std::move (launcherNodes_))
26{
27 assert (arrangerNode || ! launcherNodes.empty());
28 setOptimisations ({ tracktion::graph::ClearBuffers::yes,
29 tracktion::graph::AllocateAudioBuffer::yes });
30
31 launcherNodesCopy.reserve (launcherNodes.size());
32 std::transform (launcherNodes.begin(), launcherNodes.end(), std::back_inserter (launcherNodesCopy),
33 [] (auto& n) { return n.get(); });
34 assert (launcherNodesCopy.size() == launcherNodes.size());
35 assert (! contains_v (launcherNodesCopy, nullptr));
36}
37
38//==============================================================================
39tracktion::graph::NodeProperties ArrangerLauncherSwitchingNode::getNodeProperties()
40{
41 constexpr size_t seed = 7653239033668669842; // std::hash<std::string_view>{} ("ArrangerLauncherSwitchingNode"sv)
42 NodeProperties props = { .nodeID = hash (seed, track->itemID) };
43
45 for (auto n : getDirectInputNodes()) nodes.push_back (n);
46 for (auto n : getInternalNodes()) nodes.push_back (n);
47
48 for (auto n : nodes)
49 {
50 auto nodeProps = n->getNodeProperties();
51 props.hasAudio = props.hasAudio || nodeProps.hasAudio;
52 props.hasMidi = props.hasMidi || nodeProps.hasMidi;
53 props.numberOfChannels = std::max (props.numberOfChannels, nodeProps.numberOfChannels);
54 props.latencyNumSamples = std::max (props.latencyNumSamples, nodeProps.latencyNumSamples);
55 hash_combine (props.nodeID, nodeProps.nodeID);
56 }
57
58 return props;
59}
60
61std::vector<tracktion::graph::Node*> ArrangerLauncherSwitchingNode::getDirectInputNodes()
62{
64
65 if (arrangerNode)
66 nodes.push_back (arrangerNode.get());
67
68 return nodes;
69}
70
71std::vector<Node*> ArrangerLauncherSwitchingNode::getInternalNodes()
72{
74
75 for (auto& n : launcherNodes)
76 nodes.push_back (static_cast<Node*> (n.get()));
77
78 return nodes;
79}
80
81void ArrangerLauncherSwitchingNode::prepareToPlay (const PlaybackInitialisationInfo& info)
82{
83 const auto props = getNodeProperties();
84 const int numChannels = props.numberOfChannels;
85
86 if (auto oldGraph = info.nodeGraphToReplace)
87 {
88 if (auto oldNode = findNodeWithID<ArrangerLauncherSwitchingNode> (*oldGraph, props.nodeID))
89 {
90 if (oldNode->launcherSampleFader && oldNode->launcherSampleFader->getNumChannels() == static_cast<size_t> (numChannels))
91 launcherSampleFader = oldNode->launcherSampleFader;
92
93 if (oldNode->arrangerSampleFader && oldNode->arrangerSampleFader->getNumChannels() == static_cast<size_t> (numChannels))
94 arrangerSampleFader = oldNode->arrangerSampleFader;
95
96 if (oldNode->arrangerActiveNoteList)
97 arrangerActiveNoteList = oldNode->arrangerActiveNoteList;
98
99 if (oldNode->activeNode)
100 activeNode = oldNode->activeNode;
101
102 midiSourceID = oldNode->midiSourceID;
103 }
104 }
105
106 if (! launcherSampleFader)
107 launcherSampleFader = std::make_shared<SampleFader> (numChannels);
108
109 if (! arrangerSampleFader)
110 arrangerSampleFader = std::make_shared<SampleFader> (numChannels);
111
112 if (! arrangerActiveNoteList)
113 arrangerActiveNoteList = std::make_shared<ActiveNoteList>();
114
115 if (! activeNode)
117
118 for (auto& launcherNode : launcherNodes)
119 launcherNode->initialise (info);
120}
121
122bool ArrangerLauncherSwitchingNode::isReadyToProcess()
123{
124 return ! arrangerNode || arrangerNode->hasProcessed();
125}
126
127void ArrangerLauncherSwitchingNode::prefetchBlock (juce::Range<int64_t> referenceSampleRange)
128{
129 for (auto& launcherNode : launcherNodes)
130 launcherNode->prepareForNextBlock (referenceSampleRange);
131}
132
133void ArrangerLauncherSwitchingNode::process (ProcessContext& pc)
134{
135 activeNode->store (this, std::memory_order_release);
136
137 auto destAudioView = pc.buffers.audio;
138 assert (destAudioView.getNumChannels() == launcherSampleFader->getNumChannels());
139
140 // Logic for determining what slots/arranger to play
141 // - Iterate the slots to see if any are playing or are queued to play this block
142 // - If they are playing, only play the slots and skip the arranger
143 // - If a slot is queued, and its start position is within this block:
144 // - Play the slot
145 // - If the playSlotClips prop is false, play the arranger, but fade out at the slot start position
146 // - If no slots are playing or queued AND the playSlotClips prop is false
147 // - Play the arranger
148 // - If we've just started playing the arranger, fade out the last slot samples and fade in the arranger
149 const auto editBeatRange = getEditBeatRange();
150 const auto playArranger = ! track->playSlotClips.get();
151 const auto slotStatus = getSlotsStatus (launcherNodes,
152 editBeatRange,
153 getProcessState().getSyncPoint().monotonicBeat);
154
155 launcherSampleFader->apply (destAudioView, SampleFader::FadeType::fadeOut);
156 arrangerSampleFader->apply (destAudioView, SampleFader::FadeType::fadeOut);
157
158 processLauncher (pc, slotStatus);
159
160 if (playArranger)
161 processArranger (pc, slotStatus);
162}
163
164//==============================================================================
165void ArrangerLauncherSwitchingNode::processLauncher (ProcessContext& pc, const SlotClipStatus& slotStatus)
166{
167 auto destAudioView = pc.buffers.audio;
168 const auto numFrames = destAudioView.getNumFrames();
169 const auto editBeatRange = getEditBeatRange();
170
171 if (! launcherNodes.empty())
172 {
173 sortPlayingOrQueuedClipsFirst();
174
175 if (slotStatus.anyClipsPlaying || slotStatus.anyClipsQueued)
176 {
177 for (auto& launcherNode : launcherNodes)
178 {
179 using enum LaunchHandle::PlayState;
180 using enum LaunchHandle::QueueState;
181 const auto& lh = launcherNode->getLaunchHandle();
182 const bool slotWasPlaying = lh.getPlayingStatus() == playing;
183 const bool slotWasQueued = lh.getQueuedStatus() == playQueued;
184
185 if (! (slotWasPlaying || slotWasQueued))
186 continue;
187
188 launcherNode->Node::process (pc.numSamples, pc.referenceSampleRange);
189
190 const bool slotIsPlaying = lh.getPlayingStatus() == playing;
191 auto sourceBuffers = launcherNode->getProcessedOutput();
192 const auto numSourceChannels = sourceBuffers.audio.getNumChannels();
193
194 // We can add the whole block here as if the slot is stopped, part of the buffer will just be silent
195 choc::buffer::add (destAudioView.getFirstChannels (numSourceChannels), sourceBuffers.audio);
196 pc.buffers.midi.mergeFrom (sourceBuffers.midi);
197
198 if (slotWasPlaying && ! slotIsPlaying)
199 {
200 // Ramp out last 10 samples
201 const auto endFrame = beatToSamplePosition (slotStatus.beatsUntilQueuedStopTrimmedToBlock,
202 editBeatRange.getLength(), numFrames);
203 launcherSampleFader->trigger (10);
204 launcherSampleFader->applyAt (destAudioView, endFrame, SampleFader::FadeType::fadeOut);
205 }
206 }
207 }
208 }
209
210 launcherSampleFader->push (destAudioView);
211}
212
213void ArrangerLauncherSwitchingNode::processArranger (ProcessContext& pc, const SlotClipStatus& slotStatus)
214{
215 if (! arrangerNode)
216 return;
217
218 auto destAudioView = pc.buffers.audio;
219 const auto editBeatRange = getEditBeatRange();
220 const auto numFrames = destAudioView.getNumFrames();
221
222 auto sourceBuffers = arrangerNode->getProcessedOutput();
223 const auto numSourceChannels = sourceBuffers.audio.getNumChannels();
224
225 if (slotStatus.beatsUntilQueuedStartTrimmedToBlock)
226 {
227 // Arranger about to stop so only use some of the buffer and trigger fade out
228 if (numSourceChannels > 0)
229 {
230 const auto endFrame = beatToSamplePosition (slotStatus.beatsUntilQueuedStartTrimmedToBlock,
231 editBeatRange.getLength(), numFrames);
232
233 auto destSubView = destAudioView.getFirstChannels (numSourceChannels).getStart (endFrame);
234 auto sourceSubView = sourceBuffers.audio.getStart (endFrame);
235 arrangerSampleFader->trigger (10);
236
237 if (sourceSubView.getNumFrames() > 0)
238 {
239 arrangerSampleFader->push (sourceSubView);
240
241 choc::buffer::add (destSubView, sourceSubView);
242 launcherSampleFader->applyAt (destAudioView, endFrame, SampleFader::FadeType::fadeOut);
243 }
244
245 const auto endTime = TimePosition::fromSamples (endFrame, getSampleRate());
246
247 if (sourceBuffers.midi.isNotEmpty())
248 {
249 pc.buffers.midi.isAllNotesOff = sourceBuffers.midi.isAllNotesOff;
250
251 for (auto& m : sourceBuffers.midi)
252 {
253 if (m.getTimeStamp() > endTime.inSeconds())
254 continue;
255
256 pc.buffers.midi.add (m);
257
258 if (m.isNoteOn())
259 arrangerActiveNoteList->startNote (m.getChannel(), m.getNoteNumber());
260 else if (m.isNoteOff())
261 arrangerActiveNoteList->clearNote (m.getChannel(), m.getNoteNumber());
262 }
263 }
264
265 MidiNodeHelpers::createNoteOffs (*arrangerActiveNoteList,
266 pc.buffers.midi,
267 midiSourceID,
268 endTime.inSeconds(),
269 getPlayHead().isPlaying());
270 }
271 }
272 else
273 {
274 if (numSourceChannels > 0)
275 {
276 arrangerSampleFader->push (sourceBuffers.audio);
277 choc::buffer::add (destAudioView.getFirstChannels (numSourceChannels), sourceBuffers.audio);
278 }
279
280 if (sourceBuffers.midi.isNotEmpty())
281 {
282 pc.buffers.midi.isAllNotesOff = sourceBuffers.midi.isAllNotesOff;
283
284 for (auto& m : sourceBuffers.midi)
285 {
286 pc.buffers.midi.add (m);
287
288 if (m.isNoteOn())
289 arrangerActiveNoteList->startNote (m.getChannel(), m.getNoteNumber());
290 else if (m.isNoteOff())
291 arrangerActiveNoteList->clearNote (m.getChannel(), m.getNoteNumber());
292 }
293 }
294 }
295}
296
297void ArrangerLauncherSwitchingNode::sortPlayingOrQueuedClipsFirst()
298{
299 using enum LaunchHandle::PlayState;
300 using enum LaunchHandle::QueueState;
301 sort (launcherNodes,
302 [](auto& n1, auto& n2)
303 {
304 auto& lh1 = n1->getLaunchHandle();
305 auto& lh2 = n2->getLaunchHandle();
306
307 if (lh1.getPlayingStatus() == playing)
308 return true;
309
310 if (auto q1 = lh1.getQueuedStatus(); q1 == playQueued)
311 return lh2.getPlayingStatus() != playing;
312
313 return false;
314 });
315}
316
317void ArrangerLauncherSwitchingNode::updatePlaySlotsState()
318{
319 for (auto& n : launcherNodesCopy)
320 {
321 if (auto lh = n->getLaunchHandleIfNotUnique())
322 {
323 if (lh->getPlayingStatus() == LaunchHandle::PlayState::playing)
324 {
325 track->playSlotClips = true;
326 return;
327 }
328 }
329 }
330}
331
332//==============================================================================
333choc::buffer::FrameCount ArrangerLauncherSwitchingNode::beatToSamplePosition (std::optional<BeatDuration> beat, BeatDuration numBeats, choc::buffer::FrameCount numFrames)
334{
335 if (! beat)
336 return 0;
337
338 if (numBeats == 0_bd)
339 return 0;
340
341 const auto framesPerBeats = numFrames / numBeats.inBeats();
342 return static_cast<choc::buffer::FrameCount> (std::round (beat->inBeats() * framesPerBeats));
343}
344
345ArrangerLauncherSwitchingNode::SlotClipStatus ArrangerLauncherSwitchingNode::getSlotsStatus (const std::vector<std::unique_ptr<SlotControlNode>>& launcherNodes,
346 BeatRange editBeatRange, MonotonicBeat monotonicBeat)
347{
348 SlotClipStatus status;
349
350 const BeatRange blockRange (monotonicBeat.v, editBeatRange.getLength());
351
352 for (auto& n : launcherNodes)
353 {
354 const auto& lh = n->getLaunchHandle();
355
356 if (lh.getPlayingStatus() == LaunchHandle::PlayState::playing)
357 status.anyClipsPlaying = true;
358
359 if (lh.getQueuedStatus() == LaunchHandle::QueueState::playQueued)
360 {
361 status.anyClipsQueued = true;
362
363 const auto queuedPos = lh.getQueuedEventPosition();
364
365 if (! queuedPos)
366 {
367 status.beatsUntilQueuedStartTrimmedToBlock = 0_bd;
368 status.beatsUntilQueuedStart = 0_bd;
369 }
370
371 if (blockRange.contains (queuedPos->v))
372 status.beatsUntilQueuedStartTrimmedToBlock = queuedPos->v - blockRange.getStart();
373 }
374
375 if (lh.getQueuedStatus() == LaunchHandle::QueueState::stopQueued)
376 {
377 status.anyClipsQueued = true;
378 const auto queuedPos = lh.getQueuedEventPosition();
379
380 if (! queuedPos)
381 status.beatsUntilQueuedStopTrimmedToBlock = 0_bd;
382
383 if (queuedPos && blockRange.contains (queuedPos->v))
384 status.beatsUntilQueuedStopTrimmedToBlock = queuedPos->v - blockRange.getStart();
385 }
386 }
387
388 return status;
389}
390
391//==============================================================================
392void ArrangerLauncherSwitchingNode::sharedTimerCallback()
393{
394 if (activeNode && activeNode->load (std::memory_order_acquire) == this)
395 updatePlaySlotsState();
396}
397
398}} // namespace tracktion { inline namespace engine
assert
T back_inserter(T... args)
Main graph Node processor class.
Struct to describe a single iteration of a process call.
T is_pointer_v
T max(T... args)
T move(T... args)
Passed into AudioNodes when they are being initialised, to give them useful contextual information th...
RangeType< BeatPosition > BeatRange
A RangeType based on beats.
T push_back(T... args)
T round(T... args)
T sort(T... args)
Holds some really basic properties of a node.
T transform(T... args)