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

« « « Anklang Documentation
Loading...
Searching...
No Matches
tracktion_FollowActions.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
11#pragma once
12
13#include <span>
14#include "../../../3rd_party/nanorange/tracktion_nanorange.hpp"
15
16namespace tracktion::inline engine
17{
18
19//==============================================================================
21{
22 return magic_enum::enum_cast<FollowAction> (s.toStdString());
23}
24
26{
27 return std::string (magic_enum::enum_name (fa));
28}
29
30//==============================================================================
31namespace follow_action_utils
32{
34{
35 Clip::Ptr clip;
36 AudioTrack::Ptr track;
41 size_t sceneIndex = 0, groupIndex = 0, indexInGroup = 0;
43
44 size_t getIndexInValidHandles() const
45 {
46 if (auto iter = std::find (validSceneHandles.begin(), validSceneHandles.end(), launchHandle);
47 iter != validSceneHandles.end())
48 return static_cast<size_t> (std::distance (validSceneHandles.begin(), iter));
49
50 assert (false);
51 return 0;
52 }
53
55 {
56 return groups[groupIndex];
57 }
58
60 {
61 return groupIndex == 0 ? nullptr : &groups[groupIndex - 1];
62 }
63
65 {
66 return groupIndex == (groups.size() - 1) ? nullptr : &groups[groupIndex + 1];
67 }
68
70 {
71 if (groups.size() == 1)
72 return nullptr;
73
74 for (;;)
75 {
76 const auto index = static_cast<size_t> (random.nextInt ({ 0, static_cast<int> (groups.size()) }));
77
78 if (index != groupIndex)
79 return &groups[static_cast<size_t> (index)];
80 }
81 }
82};
83
84inline std::shared_ptr<ClipContext> createClipContext (Clip& c)
85{
86 auto sc = makeSafeRef (c);
87 auto audioTrack = dynamic_cast<AudioTrack*> (c.getTrack());
88
89 if (! audioTrack)
90 return {};
91
93 ctx->clip = c;
94 ctx->track = audioTrack;
95 ctx->launchHandle = c.getLaunchHandle();
96
98
99 for (auto cs : audioTrack->getClipSlotList().getClipSlots())
100 {
101 if (auto clip = cs->getClip())
102 {
103 auto lh = clip->getLaunchHandle();
104 allSceneHandles.push_back (lh);
105
106 if (lh)
107 ctx->validSceneHandles.emplace_back (std::move (lh));
108 }
109 else
110 {
111 allSceneHandles.push_back (nullptr);
112 }
113 }
114
115 if (ctx->validSceneHandles.empty())
116 return {};
117
118 // Create groups
119 ctx->sceneIndex = static_cast<size_t> (c.getClipSlot()->getIndex());
120
121 for (auto groupView : nano::split_view (allSceneHandles, { nullptr }))
122 {
124
125 for (auto lh : groupView)
126 {
127 if (ctx->launchHandle == lh)
128 {
129 ctx->groupIndex = ctx->groups.size();
130 ctx->indexInGroup = group.size();
131 }
132
133 group.push_back (std::move (lh));
134 }
135
136 if (group.empty())
137 continue;
138
139 assert (! contains_v (group, nullptr));
140 ctx->groups.push_back (std::move (group));
141 }
142
143 assert (! ctx->groups.empty());
144 ctx->allSceneHandles = std::move (allSceneHandles);
145
146 return ctx;
147}
148
149inline std::shared_ptr<LaunchHandle> getLaunchHandle (follow_action_utils::ClipContext& ctx,
150 FollowAction followAction)
151{
152 using enum FollowAction;
153
154 assert (! ctx.getGroup().empty());
155 assert (! ctx.validSceneHandles.empty());
156
157 switch (followAction)
158 {
159 case globalReturnToArrangement: [[ fallthrough ]];
160 case trackAny: [[ fallthrough ]];
161 case trackOther: [[ fallthrough ]];
162 case currentGroupAny: [[ fallthrough ]];
163 case currentGroupOther: [[ fallthrough ]];
164 case previousGroupAny: [[ fallthrough ]];
165 case otherGroupFirst: [[ fallthrough ]];
166 case otherGroupLast: [[ fallthrough ]];
167 case nextGroupAny: [[ fallthrough ]];
168 case otherGroupAny:
169 assert (false && "These actions are random can't return launch handles");
170 break;
171
172 case none:
173 return {};
174
175 case globalStop:
176 return {};
177
178 case globalPlayAgain:
179 return ctx.launchHandle;
180
181 case trackPrevious:
182 {
183 if (auto validIndex = ctx.getIndexInValidHandles(); validIndex > 0)
184 if (auto prevHandle = ctx.validSceneHandles[validIndex - 1]; prevHandle)
185 return prevHandle;
186
187 break;
188 }
189 case trackNext:
190 {
191 if (auto validIndex = ctx.getIndexInValidHandles(); validIndex < (ctx.validSceneHandles.size() - 1))
192 if (auto nextHandle = ctx.validSceneHandles[validIndex + 1]; nextHandle)
193 return nextHandle;
194
195 break;
196 }
197 case trackFirst:
198 {
199 return ctx.validSceneHandles.front();
200 }
201 case trackLast:
202 {
203 return ctx.validSceneHandles.back();
204 }
205 case trackRoundRobin:
206 {
207 const auto validIndex = ctx.getIndexInValidHandles();
208 return ctx.validSceneHandles[(validIndex + 1) % ctx.validSceneHandles.size()];
209 }
210 case currentGroupPrevious:
211 {
212 if (ctx.indexInGroup > 0)
213 return ctx.getGroup()[(ctx.indexInGroup - 1)];
214
215 break;
216 }
217 case currentGroupNext:
218 {
219 if (ctx.indexInGroup < (ctx.getGroup().size() - 1))
220 return ctx.getGroup()[(ctx.indexInGroup + 1)];
221
222 break;
223 }
224 case currentGroupFirst:
225 {
226 return ctx.getGroup().front();
227 }
228 case currentGroupLast:
229 {
230 return ctx.getGroup().back();
231 }
232 case currentGroupRoundRobin:
233 {
234 auto &group = ctx.getGroup();
235 return group[(ctx.indexInGroup + 1) % group.size()];
236 }
237 case previousGroupFirst:
238 {
239 if (auto group = ctx.getPreviousGroup())
240 return group->front();
241
242 break;
243 }
244 case previousGroupLast:
245 {
246 if (auto group = ctx.getPreviousGroup())
247 return group->back();
248
249 break;
250 }
251 case nextGroupFirst:
252 {
253 if (auto group = ctx.getNextGroup())
254 return group->front();
255
256 break;
257 }
258 case nextGroupLast:
259 {
260 if (auto group = ctx.getNextGroup())
261 return group->back();
262
263 break;
264 }
265 };
266
267 return {};
268}
269}
270
271inline std::function<void (MonotonicBeat)> createFollowAction (std::shared_ptr<follow_action_utils::ClipContext> ctx,
272 FollowAction followAction)
273{
274 using enum FollowAction;
275
276 switch (followAction)
277 {
278 case globalReturnToArrangement:
279 {
280 return [ctx] (auto)
281 { ctx->track->playSlotClips = true; };
282 }
283 case trackAny:
284 {
285 return [ctx] (auto b)
286 {
287 const auto index = static_cast<size_t> (ctx->random.nextInt ({ 0, static_cast<int> (ctx->validSceneHandles.size()) }));
288 ctx->validSceneHandles[index]->play (b);
289 };
290 }
291 case trackOther:
292 {
293 if (ctx->validSceneHandles.size() > 1)
294 return [ctx] (auto b)
295 {
296 for (;;)
297 {
298 const auto index = static_cast<size_t> (ctx->random.nextInt ({ 0, static_cast<int> (ctx->validSceneHandles.size()) }));
299
300 if (index == ctx->sceneIndex)
301 continue;
302
303 ctx->validSceneHandles[index]->play (b);
304 break;
305 }
306 };
307
308 break;
309 }
310 case currentGroupAny:
311 {
312 return [ctx, &group = ctx->getGroup()] (auto b)
313 {
314 const auto index = static_cast<size_t> (ctx->random.nextInt ({ 0, static_cast<int> (group.size()) }));
315 group[index]->play (b);
316 };
317 }
318 case currentGroupOther:
319 {
320 if (ctx->getGroup().size() > 1)
321 return [ctx, &group = ctx->getGroup()] (auto b)
322 {
323 for (;;)
324 {
325 const auto index = static_cast<size_t> (ctx->random.nextInt ({ 0, static_cast<int> (group.size()) }));
326
327 if (index == ctx->indexInGroup)
328 continue;
329
330 group[index]->play (b);
331 break;
332 }
333 };
334
335 break;
336 }
337 case previousGroupAny:
338 {
339 if (auto group = ctx->getPreviousGroup())
340 return [ctx, group] (auto b)
341 {
342 const auto index = static_cast<size_t> (ctx->random.nextInt ({ 0, static_cast<int> (group->size()) }));
343 (*group)[index]->play (b);
344 };
345
346 break;
347 }
348 case nextGroupAny:
349 {
350 if (auto group = ctx->getNextGroup())
351 return [ctx, group] (auto b)
352 {
353 const auto index = static_cast<size_t> (ctx->random.nextInt ({ 0, static_cast<int> (group->size()) }));
354 (*group)[index]->play (b);
355 };
356
357 break;
358 }
359 case otherGroupFirst:
360 {
361 if (ctx->groups.size() > 1)
362 return [ctx] (auto b)
363 { ctx->getOtherGroup()->front()->play (b); };
364
365 break;
366 }
367 case otherGroupLast:
368 {
369 if (ctx->groups.size() > 1)
370 return [ctx] (auto b)
371 { ctx->getOtherGroup()->back()->play (b); };
372
373 break;
374 }
375 case otherGroupAny:
376 {
377 if (ctx->groups.size() > 1)
378 return [ctx] (auto b)
379 {
380 auto group = ctx->getOtherGroup();
381 const auto index = static_cast<size_t> (ctx->random.nextInt ({ 0, static_cast<int> (group->size()) }));
382 (*group)[index]->play (b);
383 };
384
385 break;
386 }
387 case none: [[ fallthrough ]];
388 case globalStop: [[ fallthrough ]];
389 case globalPlayAgain: [[ fallthrough ]];
390 case trackPrevious: [[ fallthrough ]];
391 case trackNext: [[ fallthrough ]];
392 case trackFirst: [[ fallthrough ]];
393 case trackLast: [[ fallthrough ]];
394 case trackRoundRobin: [[ fallthrough ]];
395 case currentGroupPrevious: [[ fallthrough ]];
396 case currentGroupNext: [[ fallthrough ]];
397 case currentGroupFirst: [[ fallthrough ]];
398 case currentGroupLast: [[ fallthrough ]];
399 case currentGroupRoundRobin: [[ fallthrough ]];
400 case previousGroupFirst: [[ fallthrough ]];
401 case previousGroupLast: [[ fallthrough ]];
402 case nextGroupFirst: [[ fallthrough ]];
403 case nextGroupLast:
404 {
405 // All these are know at graph build time
406 if (auto lh = getLaunchHandle (*ctx, followAction))
407 return [lh] (auto b) { lh->play (b); };
408
409 break;
410 }
411 };
412
413 return {};
414}
415
416std::function<void (MonotonicBeat)> createFollowAction (Clip& c)
417{
418 auto followActions = c.getFollowActions();
419
420 if (! followActions)
421 return {};
422
423 auto actions = followActions->getActions();
424
425 if (actions.empty())
426 return {};
427
428 auto ctx = follow_action_utils::createClipContext (c);
429
430 // Create actions with probabilities
431 if (actions.size() == 1)
432 return createFollowAction (ctx, followActions->getActions().front()->action.get());
433
434 struct FollowActionContainer
435 {
436 juce::Range<double> probabilityRange;
437 std::function<void (MonotonicBeat)> action;
438 };
439
440 std::vector<FollowActionContainer> followActionContainers;
441 double maxProbability = 0.0;
442
443 for (auto action : actions)
444 {
445 FollowActionContainer container;
446 container.probabilityRange = { maxProbability, maxProbability + action->weight };
447 container.action = createFollowAction (ctx, action->action);
448 followActionContainers.push_back (std::move (container));
449
450 maxProbability = container.probabilityRange.getEnd();
451 }
452
453 return [followActionContainers = std::move (followActionContainers), ctx, maxProbability] (MonotonicBeat beat)
454 {
455 const auto randomValue = ctx->random.nextFloat() * maxProbability;
456
457 for (auto& actionContainer : followActionContainers)
458 {
459 if (! actionContainer.probabilityRange.contains (randomValue))
460 continue;
461
462 if (actionContainer.action)
463 return actionContainer.action (beat);
464
465 return;
466 }
467 };
468}
469
470
471//==============================================================================
472//==============================================================================
474{
475public:
476 List (FollowActions& fa, juce::ValueTree listState)
477 : ValueTreeObjectList<Action> (std::move (listState)),
478 followActions (fa)
479 {
480 rebuildObjects();
481 }
482
483 ~List() override
484 {
485 freeObjects();
486 }
487
488 bool isSuitableType (const juce::ValueTree& v) const override
489 {
490 return v.hasType (IDs::ACTION);
491 }
492
493 Action* createNewObject (const juce::ValueTree& newState) override
494 {
495 auto a = new Action();
496
497 auto v = newState;
498 a->action.referTo (v, IDs::type, followActions.undoManager, FollowAction::currentGroupRoundRobin);
499 a->weight.referTo (v, IDs::weight, followActions.undoManager, 1.0);
500 a->state = std::move (v);
501
502 return a;
503 }
504
505 void deleteObject (Action* a) override
506 {
507 delete a;
508 }
509
510 void newObjectAdded (Action*) override
511 {
512 followActions.sendChangeMessage();
513 }
514
515 void objectRemoved (Action*) override
516 {
517 followActions.sendSynchronousChangeMessage();
518 }
519
520 void objectOrderChanged() override
521 {
522 followActions.sendChangeMessage();
523 }
524
525private:
526 FollowActions& followActions;
527};
528
529//==============================================================================
530FollowActions::FollowActions (juce::ValueTree v, juce::UndoManager* um)
531 : state (std::move (v)), undoManager (um)
532{
533 list = std::make_unique<List> (*this, state);
534}
535
536FollowActions::~FollowActions()
537{
538}
539
540FollowActions::Action& FollowActions::addAction()
541{
542 state.appendChild ({ IDs::ACTION, {} }, undoManager);
543 return *getActions().back();
544}
545
546void FollowActions::removeAction (Action& actionToRemove)
547{
548 auto actionState = actionToRemove.action.getValueTree();
549 actionState.getParent().removeChild (actionState, undoManager);
550}
551
552std::span<FollowActions::Action*> FollowActions::getActions() const
553{
554 return { list->begin(), list->end() };
555}
556
557}
assert
T back(T... args)
T begin(T... args)
void appendChild(const ValueTree &child, UndoManager *undoManager)
T distance(T... args)
T empty(T... args)
T end(T... args)
T find(T... args)
T front(T... args)
random
T is_pointer_v
T push_back(T... args)
T size(T... args)
std::optional< FollowAction > followActionFromString(juce::String s)
Converts a string to a FollowAction if possible.
FollowAction
Determines the type of action to perform after a Clip has played for a set period.