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

« « « Anklang Documentation
Loading...
Searching...
No Matches
tracktion_AbletonLink.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
11namespace tracktion { inline namespace engine
12{
13
15{
17 : transport (t)
18 {}
19
20 bool isConnected() const
21 {
22 return numPeers > 0;
23 }
24
25 void activateTimer (bool isActive)
26 {
27 if (isActive)
28 {
29 startTimer (12);
30 }
31 else
32 {
33 stopTimer();
34 setSpeedCompensation (0.0);
35 }
36 }
37
38 void syncronise (TimePosition pos)
39 {
40 if (! isConnected() || ! transport.isPlaying())
41 return;
42
43 // Get the current tempo info
44 const auto& seq = transport.edit.tempoSequence.getInternalSequence();
45 const auto localBarsAndBeatsPos = seq.toBarsAndBeats (pos);
46 const auto numerator = static_cast<double> (localBarsAndBeatsPos.numerator);
47
48 // Calculate any device latency and custom latency
49 const double bps = seq.getBeatsPerSecondAt (pos).v;
50 auto& dm = transport.engine.getDeviceManager();
51 const auto outputLatencyBeats = ((dm.getBlockLength() * 2).inSeconds() + dm.getOutputLatencySeconds()) * bps;
52 const auto customOffsetBeats = (customOffsetMs / 1000.0) * bps;
53
54 // Calculate the bar phase from the transport position
55 const auto outputLatencyPhase = outputLatencyBeats / numerator;
56 const auto customOffsetPhase = customOffsetBeats / numerator;
57
58 auto localPhase = (localBarsAndBeatsPos.beats.inBeats() / numerator)
59 - outputLatencyPhase + customOffsetPhase;
60 localPhase = negativeAwareFmod (localPhase, 1.0);
61
62 const auto linkPhase = getBarPhase (numerator) / numerator;
63 linkBarPhase = linkPhase;
64
65 // Find the phase offset between tracktion
66 // If this is 0 this means perfect phase, > 0 means link is ahead, < 0 means tracktion is ahead
67 auto offsetPhase = circularDifference (linkPhase, localPhase);
68 chaseProportion = offsetPhase;
69
70 const auto offsetBeats = offsetPhase * numerator;
71 const auto timeNow = juce::Time::getMillisecondCounter();
72
73 // If we're out of sync by more than a beat, re-sync by jumping
74 if (std::abs (offsetBeats) >= (numerator / 2.0) && timeNow > inhibitTimer)
75 {
76 inhibitTimer = timeNow + 250;
77
78 setSpeedCompensation (0.0);
79 listeners.call (&Listener::linkRequestedPositionChange, BeatDuration::fromBeats (offsetBeats));
80 }
81 else
82 {
83 setSpeedCompensation (offsetPhase);
84 }
85 }
86
87 void timerCallback() override
88 {
89 }
90
91 void setTempoFromLink (double bpm)
92 {
93 bpm = getTempoInRange (bpm);
94
95 juce::MessageManager::callAsync ([this, editRef = makeSafeRef (transport.edit), bpm]
96 {
97 if (editRef != nullptr)
98 {
99 inhibitTimer = juce::Time::getMillisecondCounter() + 100;
100 listeners.call (&Listener::linkRequestedTempoChange, bpm);
101 }
102 });
103 }
104
105 void setStartStopFromLink (bool isPlaying)
106 {
107 juce::MessageManager::callAsync ([this, editRef = makeSafeRef (transport.edit), isPlaying]
108 {
109 if (editRef != nullptr)
110 {
111 inhibitTimer = juce::Time::getMillisecondCounter() + 100;
112 listeners.call (&Listener::linkRequestedStartStopChange, isPlaying);
113 }
114 });
115 }
116
117 void callConnectionChanged()
118 {
119 juce::MessageManager::callAsync ([this, editRef = makeSafeRef (transport.edit)]
120 {
121 if (editRef != nullptr)
122 listeners.call (&AbletonLink::Listener::linkConnectionChanged);
123 });
124 }
125
126 double getBeatsUntilNextCycle (double quantum)
127 {
128 return quantum - getBarPhase (quantum);
129 }
130
131 void setCustomOffset (int offsetMs)
132 {
133 customOffsetMs = offsetMs;
134 }
135
136 void setTempoConstraint (juce::Range<double> minMaxTempo)
137 {
138 allowedTempos = minMaxTempo;
139
140 if (isConnected())
141 setTempoFromLink (getTempoFromLink());
142 }
143
144 double getTempoInRange (double bpm) const
145 {
146 if (bpm == 0.0)
147 return 0;
148
149 const auto isTooQuick = [topBpm = allowedTempos.getEnd()] (double b) { return b >= topBpm; };
150 const bool wasTooQuick = isTooQuick (bpm); // Avoid infinite loops
151
152 while (isTempoOutOfRange (bpm) && (wasTooQuick == isTooQuick (bpm)))
153 bpm *= wasTooQuick ? 0.5 : 2.0;
154
155 jassert (! isTempoOutOfRange (bpm)); // Tempo can't be halved/doubled to fit in the range
156
157 return allowedTempos.clipValue (bpm);
158 }
159
160 bool isTempoOutOfRange (double bpm) const
161 {
162 return ! (allowedTempos.getStart() <= bpm && bpm <= allowedTempos.getEnd());
163 }
164
165 virtual bool isEnabled() const = 0;
166 virtual void setEnabled (bool) = 0;
167 virtual bool isPlaying() const = 0;
168 virtual void enableStartStopSync (bool) = 0;
169 virtual bool getStartStopSyncEnabledFromLink() const = 0;
170 virtual void setStartStopToLink (bool) = 0;
171 virtual void setTempoToLink (double bpm) = 0;
172 virtual double getTempoFromLink() = 0;
173 virtual double getBeatNow (double quantum) = 0;
174 virtual double getBarPhase (double quantum) = 0;
175
176 void addListener (Listener* l) { listeners.add (l); }
177 void removeListener (Listener* l) { listeners.remove (l); }
178
179 static inline double negativeAwareFmod (double a, double b)
180 {
181 return a - b * std::floor (a / b);
182 }
183
184 static inline double circularDifference (double a, double b)
185 {
186 double diff = a - b;
187
188 if (diff < -0.5) return diff + 1.0;
189 if (diff > 0.5) return diff - 1.0;
190
191 return diff;
192 }
193
194 void setSpeedCompensation (double phaseProportion)
195 {
196 if (auto epc = transport.getCurrentPlaybackContext())
197 epc->setTempoAdjustment (phaseProportion * 10.0);
198 }
199
200 TransportControl& transport;
202 std::atomic<size_t> numPeers { 0 };
203 std::atomic<int> customOffsetMs { 0 };
204 uint32_t inhibitTimer = 0;
205 std::atomic<double> linkBarPhase { 0.0 }, chaseProportion { 0.0 };
206
207 juce::Range<double> allowedTempos { 0.0, 999.0 };
208
210};
211
212#if TRACKTION_ENABLE_ABLETON_LINK
213
214#if (JUCE_WINDOWS || JUCE_MAC || JUCE_LINUX || JUCE_ANDROID)
215 //==========================================================================
216 struct LinkImpl : public AbletonLink::ImplBase
217 {
218 LinkImpl (TransportControl& t)
219 : AbletonLink::ImplBase (t)
220 {
221 link.setNumPeersCallback ([this] (std::size_t n) { numPeersCallback (n); });
222 link.setTempoCallback ([this] (double bpm) { setTempoFromLink (bpm); });
223 link.setStartStopCallback ([this] (bool isPlaying) { setStartStopFromLink (isPlaying); });
224 }
225
226 ~LinkImpl() override
227 {
228 link.setNumPeersCallback ([] (std::size_t) {});
229 link.setTempoCallback ([] (double) {});
230 link.setStartStopCallback ([] (bool) {});
231 }
232
233 bool isEnabled() const override
234 {
235 TRACKTION_ASSERT_MESSAGE_THREAD
236 return link.isEnabled();
237 }
238
239 void setEnabled (bool isEnabled) override
240 {
241 TRACKTION_ASSERT_MESSAGE_THREAD
242 link.enable (isEnabled);
243 activateTimer (isEnabled);
244
245 if (isEnabled)
246 setTempoFromLink (link.captureAppSessionState().tempo());
247 }
248
249 bool isPlaying() const override
250 {
251 TRACKTION_ASSERT_MESSAGE_THREAD
252 return link.captureAppSessionState().isPlaying();
253 }
254
255 void enableStartStopSync (bool enable) override
256 {
257 TRACKTION_ASSERT_MESSAGE_THREAD
258 link.enableStartStopSync (enable);
259 }
260
261 bool getStartStopSyncEnabledFromLink() const override
262 {
263 TRACKTION_ASSERT_MESSAGE_THREAD
264 return link.isStartStopSyncEnabled();
265 }
266
267 void numPeersCallback (std::size_t newNumPeers)
268 {
269 jassert (! juce::MessageManager::existsAndIsCurrentThread());
270 numPeers = newNumPeers;
271 callConnectionChanged();
272
273 if (isConnected())
274 setTempoFromLink (link.captureAudioSessionState().tempo());
275 }
276
277 void setStartStopToLink (bool isPlaying) override
278 {
279 TRACKTION_ASSERT_MESSAGE_THREAD
280 auto state = link.captureAppSessionState();
281 state.setIsPlaying (isPlaying, clock.micros());
282 link.commitAppSessionState (state);
283 }
284
285 void setTempoToLink (double bpm) override
286 {
287 TRACKTION_ASSERT_MESSAGE_THREAD
288 auto state = link.captureAppSessionState();
289 state.setTempo (bpm, clock.micros());
290 link.commitAppSessionState (state);
291 }
292
293 double getTempoFromLink() override
294 {
295 jassert (! juce::MessageManager::existsAndIsCurrentThread());
296 return link.captureAudioSessionState().tempo();
297 }
298
299 double getBeatNow (double quantum) override
300 {
301 jassert (! juce::MessageManager::existsAndIsCurrentThread());
302 return link.captureAudioSessionState().beatAtTime (clock.micros(), quantum);
303 }
304
305 double getBarPhase (double quantum) override
306 {
307 if (juce::MessageManager::existsAndIsCurrentThread())
308 return link.captureAppSessionState().phaseAtTime (clock.micros(), quantum);
309
310 return link.captureAudioSessionState().phaseAtTime (clock.micros(), quantum);
311 }
312
313 ableton::Link::Clock clock;
314 ableton::Link link { 120 };
315 };
316
317#elif JUCE_IOS
318
319 // To use Link on iOS you need to get access to the LinkKit repo from
320 // Ableton, add its include folder to your header search paths, and link to
321 // the libABLLink.a static library.
322 #include "ABLLink.h"
323
324 struct LinkImpl : public AbletonLink::ImplBase
325 {
326 LinkImpl (TransportControl& t)
327 : AbletonLink::ImplBase (t),
328 link (ABLLinkNew (120))
329 {
330 ABLLinkSetSessionTempoCallback (link, tempoChangedCallback, this);
331 ABLLinkSetIsConnectedCallback (link, isConnectedCallback, this);
332 ABLLinkSetIsEnabledCallback (link, isEnabledCallback, this);
333 ABLLinkSetStartStopCallback (link, startStopCallback, this);
334
335 setEnabled (isActive);
336 }
337
338 ~LinkImpl() override
339 {
340 ABLLinkDelete (link);
341 }
342
343 bool isEnabled() const override
344 {
345 TRACKTION_ASSERT_MESSAGE_THREAD
346 return ABLLinkIsEnabled (link) && isActive;
347 }
348
349 void setEnabled (bool isEnabled) override
350 {
351 TRACKTION_ASSERT_MESSAGE_THREAD
352 isActive = isEnabled;
353 ABLLinkSetActive (link, isEnabled);
354
355 // We don't necessarily get an isConnectedCallback callback after
356 // enabling, make sure everything is up to date.
357 Timer::callAfterDelay (500, [this, editRef = makeSafeRef (transport.edit)]
358 {
359 if (editRef != nullptr)
360 isConnectedCallback (ABLLinkIsConnected (link), this);
361 });
362 }
363
364 bool isPlaying() const override
365 {
366 TRACKTION_ASSERT_MESSAGE_THREAD
367 return ABLLinkIsPlaying (ABLLinkCaptureAppSessionState (link));
368 }
369
370 void enableStartStopSync (bool) override
371 {
372 jassertfalse; // This is only settable via the system prefs
373 }
374
375 bool getStartStopSyncEnabledFromLink() const override
376 {
377 TRACKTION_ASSERT_MESSAGE_THREAD
378 return ABLLinkIsStartStopSyncEnabled (link);
379 }
380
381 void setStartStopToLink (bool isPlaying) override
382 {
383 TRACKTION_ASSERT_MESSAGE_THREAD
384 auto state = ABLLinkCaptureAppSessionState (link);
385 ABLLinkSetIsPlaying (state, isPlaying, (std::uint64_t) juce::Time::getHighResolutionTicks());
386 ABLLinkCommitAppSessionState (link, state);
387 }
388
389 void setTempoToLink (double bpm) override
390 {
391 TRACKTION_ASSERT_MESSAGE_THREAD
392 auto state = ABLLinkCaptureAppSessionState (link);
393 ABLLinkSetTempo (state, bpm, (std::uint64_t) juce::Time::getHighResolutionTicks());
394 ABLLinkCommitAppSessionState (link, state);
395 }
396
397 double getTempoFromLink() override
398 {
399 jassert (! juce::MessageManager::existsAndIsCurrentThread());
400 return ABLLinkGetTempo (ABLLinkCaptureAudioSessionState (link));
401 }
402
403 double getBeatNow (double quantum) override
404 {
405 jassert (! juce::MessageManager::existsAndIsCurrentThread());
406 auto state = ABLLinkCaptureAudioSessionState (link);
407 return ABLLinkBeatAtTime (state, (std::uint64_t) juce::Time::getHighResolutionTicks(), quantum);
408 }
409
410 double getBarPhase (double quantum) override
411 {
413 ? ABLLinkCaptureAppSessionState (link)
414 : ABLLinkCaptureAudioSessionState (link);
415
416 return ABLLinkPhaseAtTime (state, (std::uint64_t) juce::Time::getHighResolutionTicks(), quantum);
417 }
418
419 static void tempoChangedCallback (double bpm, void *context)
420 {
421 auto* thisPtr = static_cast<LinkImpl*> (context);
422 thisPtr->setTempoFromLink (bpm);
423 }
424
425 static void isConnectedCallback (bool isConnected, void *context)
426 {
427 auto* thisPtr = static_cast<LinkImpl*> (context);
428
429 thisPtr->numPeers = (size_t) (isConnected ? 1 : 0);
430 thisPtr->activateTimer (isConnected);
431
432 isEnabledCallback (isConnected, context);
433 }
434
435 static void isEnabledCallback (bool isEnabled, void *context)
436 {
437 auto* thisPtr = static_cast<LinkImpl*> (context);
438
439 if (! isEnabled)
440 thisPtr->numPeers = (size_t) 0;
441
442 thisPtr->callConnectionChanged();
443
444 if (isEnabled)
445 broadcastTempo (thisPtr);
446 }
447
448 static void startStopCallback (bool isPlaying, void *context)
449 {
450 auto* thisPtr = static_cast<LinkImpl*> (context);
451 thisPtr->setStartStopFromLink (isPlaying);
452 }
453
454 static void broadcastTempo (LinkImpl* context)
455 {
456 context->setTempoFromLink (context->getTempoFromLink());
457 }
458
459 ABLLinkRef link;
460 static bool isActive; // Multiple edits may exist, Link is global
461 };
462
463 bool LinkImpl::isActive = true;
464
465#endif
466#endif // TRACKTION_ENABLE_ABLETON_LINK
467
468
469//==============================================================================
470AbletonLink::AbletonLink (TransportControl& t)
471{
472 #if TRACKTION_ENABLE_ABLETON_LINK
473 implementation = std::make_unique<LinkImpl> (t);
474 #endif
475
477}
478
479AbletonLink::~AbletonLink() {}
480
481#if TRACKTION_ENABLE_ABLETON_LINK && JUCE_IOS
482 ABLLink* AbletonLink::getLinkInstanceForIOS() { return static_cast<LinkImpl*>(implementation.get())->link; }
483#endif
484
485void AbletonLink::setEnabled (bool isEnabled)
486{
487 if (implementation != nullptr)
488 implementation->setEnabled (isEnabled);
489}
490
491bool AbletonLink::isEnabled() const
492{
493 return implementation != nullptr && implementation->isEnabled();
494}
495
496bool AbletonLink::isConnected() const
497{
498 return implementation != nullptr && implementation->isConnected();
499}
500
501size_t AbletonLink::getNumPeers() const
502{
503 return implementation != nullptr ? implementation->numPeers.load() : 0;
504}
505
506bool AbletonLink::isPlaying() const
507{
508 return implementation != nullptr && implementation->isPlaying();
509}
510
511void AbletonLink::enableStartStopSync (bool enable)
512{
513 if (implementation != nullptr)
514 implementation->enableStartStopSync (enable);
515}
516
517bool AbletonLink::isStartStopSyncEnabled() const
518{
519 return implementation != nullptr && implementation->getStartStopSyncEnabledFromLink();
520}
521
522double AbletonLink::getBeatsUntilNextCycle (double quantum) const
523{
524 if (implementation != nullptr)
525 return implementation->getBeatsUntilNextCycle (quantum);
526
527 return 0.0;
528}
529
530void AbletonLink::requestStartStopChange (bool isPlaying)
531{
532 if (implementation != nullptr)
533 implementation->setStartStopToLink (isPlaying);
534}
535
536void AbletonLink::requestTempoChange (double newBpm)
537{
538 if (implementation != nullptr)
539 implementation->setTempoToLink (newBpm);
540}
541
542void AbletonLink::setTempoConstraint (juce::Range<double> minMaxTempo)
543{
544 if (implementation != nullptr)
545 implementation->setTempoConstraint (minMaxTempo);
546}
547
548double AbletonLink::getBarPhase() const
549{
550 return implementation != nullptr ? implementation->linkBarPhase.load() : 0.0;
551}
552
553double AbletonLink::getChaseProportion() const
554{
555 return implementation != nullptr ? implementation->chaseProportion.load() : 0.0;
556}
557
558double AbletonLink::getSessionTempo() const
559{
560 if (implementation != nullptr)
561 return implementation->getTempoFromLink();
562
563 return 120.0;
564}
565
566void AbletonLink::setCustomOffset (int offsetMs)
567{
568 if (implementation != nullptr)
569 implementation->setCustomOffset (offsetMs);
570}
571
572void AbletonLink::syncronise (TimePosition pos)
573{
574 if (implementation != nullptr)
575 implementation->syncronise (pos);
576}
577
578void AbletonLink::addListener (Listener* l)
579{
580 if (implementation != nullptr)
581 implementation->addListener (l);
582}
583
584void AbletonLink::removeListener (Listener* l)
585{
586 if (implementation != nullptr)
587 implementation->removeListener (l);
588}
589
590}} // namespace tracktion { inline namespace engine
static bool existsAndIsCurrentThread() noexcept
static int64 getHighResolutionTicks() noexcept
static uint32 getMillisecondCounter() noexcept
void stopTimer() noexcept
void startTimer(int intervalInMilliseconds) noexcept
TempoSequence tempoSequence
The global TempoSequence of this Edit.
DeviceManager & getDeviceManager() const
Returns the DeviceManager instance for handling audio / MIDI devices.
const tempo::Sequence & getInternalSequence() const
N.B.
Controls the transport of an Edit's playback.
bool isPlaying() const
Returns true if the transport is playing.
Edit & edit
The Edit this transport belongs to.
Engine & engine
The Engine this Edit belongs to.
clock
T floor(T... args)
T is_pointer_v
#define jassert(expression)
#define JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(className)
#define jassertfalse
link
void ignoreUnused(Types &&...) noexcept
SafeSelectable< SelectableType > makeSafeRef(SelectableType &selectable)
Creates a SafeSelectable for a given selectable object.
typedef uint32_t
Represents a position in real-life time.
typedef size_t