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

« « « Anklang Documentation
Loading...
Searching...
No Matches
tracktion_TransportControl.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
14namespace IDs
15{
16 #define DECLARE_ID(name) const juce::Identifier name (#name);
17
18 DECLARE_ID (safeRecording)
19 DECLARE_ID (discardRecordings)
20 DECLARE_ID (clearDevices)
21 DECLARE_ID (justSendMMCIfEnabled)
22 DECLARE_ID (canSendMMCStop)
23 DECLARE_ID (allowRecordingIfNoInputsArmed)
24 DECLARE_ID (clearDevicesOnStop)
25 DECLARE_ID (updatingFromPlayHead)
26 DECLARE_ID (scrubInterval)
27
28 DECLARE_ID (userDragging)
29 DECLARE_ID (lastUserDragTime)
30 DECLARE_ID (reallocationInhibitors)
31 DECLARE_ID (playbackContextAllocation)
32
33 DECLARE_ID (rewindButtonDown)
34 DECLARE_ID (fastForwardButtonDown)
35 DECLARE_ID (nudgeLeftCount)
36 DECLARE_ID (nudgeRightCount)
37
38 DECLARE_ID (videoPosition)
39 DECLARE_ID (forceVideoJump)
40
41 #undef DECLARE_ID
42}
43
44namespace TransportHelpers
45{
46 inline TimePosition snapTime (TransportControl& tc, TimePosition t, bool invertSnap)
47 {
48 return (tc.snapToTimecode ^ invertSnap) ? tc.getSnapType().roundTimeNearest (t, tc.edit.tempoSequence)
49 : t;
50 }
51
52 inline TimePosition snapTimeUp (TransportControl& tc, TimePosition t, bool invertSnap)
53 {
54 return (tc.snapToTimecode ^ invertSnap) ? tc.getSnapType().roundTimeUp (t, tc.edit.tempoSequence)
55 : t;
56 }
57
58 inline TimePosition snapTimeDown (TransportControl& tc, TimePosition t, bool invertSnap)
59 {
60 return (tc.snapToTimecode ^ invertSnap) ? tc.getSnapType().roundTimeDown (t, tc.edit.tempoSequence)
61 : t;
62 }
63
64 inline void resyncLauncherClips (TransportControl& tc, std::optional<SyncPoint> startPoint, bool syncToStartOfBar)
65 {
66 auto epc = tc.getCurrentPlaybackContext();
67
68 if (! epc)
69 return;
70
71 auto& edit = tc.edit;
72 juce::Array<Clip*> launchedClips;
73
74 edit.clipSlotCache.visitItems ([&] (auto cs)
75 {
76 if (auto c = cs->getClip())
77 {
78 if (auto lh = c->getLaunchHandle())
79 {
80 if (lh->getQueuedStatus() == LaunchHandle::QueueState::stopQueued)
81 return;
82
83 if (lh->getPlayingStatus() == LaunchHandle::PlayState::playing)
84 launchedClips.add (c);
85 }
86 }
87 });
88
89 if (launchedClips.isEmpty())
90 return;
91
92 // If clips are starting in the future, stop them now
93 if (startPoint)
94 {
95 for (auto c : launchedClips)
96 c->getLaunchHandle()->stop ({});
97
98 epc->blockUntilSyncPointChange();
99 }
100
101 const auto currentPoint = epc->getSyncPoint();
102
103 if (! currentPoint)
104 return;
105
106 if (! startPoint)
107 startPoint = currentPoint;
108
109 auto& ts = edit.tempoSequence;
110 const auto currentBeat = ts.toBeats (tc.getPosition());
111
112 MonotonicBeat startSyncBeat;
113
114 if (syncToStartOfBar)
115 {
116 const auto currentBarsBeats = ts.toBarsAndBeats (tc.getPosition());
117 const auto barStartBeat = ts.toBeats (tempo::BarsAndBeats { .bars = currentBarsBeats.bars });
118 const auto launchBeatDiff = currentBeat - barStartBeat;
119 startSyncBeat = MonotonicBeat { currentPoint->monotonicBeat.v - launchBeatDiff };
120 }
121 else
122 {
123 const auto launchBeatDiff = currentPoint->beat - startPoint->beat;
124 startSyncBeat = MonotonicBeat { currentPoint->monotonicBeat.v - launchBeatDiff };
125 }
126
127 for (auto c : launchedClips)
128 {
129 auto lh = c->getLaunchHandle();
130 assert (lh);
131 lh->play (startSyncBeat);
132 }
133 }
134}
135
136
137//==============================================================================
142{
143 TransportState (TransportControl& tc, juce::ValueTree transportStateToUse)
144 : state (transportStateToUse), transport (tc)
145 {
146 juce::UndoManager* um = nullptr;
147
148 playing.referTo (transientState, IDs::playing, um);
149 recording.referTo (transientState, IDs::recording, um);
150 safeRecording.referTo (transientState, IDs::safeRecording, um);
151
152 discardRecordings.referTo (transientState, IDs::discardRecordings, um);
153 clearDevices.referTo (transientState, IDs::clearDevices, um);
154 justSendMMCIfEnabled.referTo (transientState, IDs::justSendMMCIfEnabled, um);
155 canSendMMCStop.referTo (transientState, IDs::canSendMMCStop, um);
156 allowRecordingIfNoInputsArmed.referTo (transientState, IDs::allowRecordingIfNoInputsArmed, um);
157 clearDevicesOnStop.referTo (transientState, IDs::clearDevicesOnStop, um);
158 updatingFromPlayHead.referTo (transientState, IDs::updatingFromPlayHead, um);
159
160 startTime.referTo (transientState, IDs::startTime, um);
161 endTime.referTo (transientState, IDs::endTime, um);
162 userDragging.referTo (transientState, IDs::userDragging, um);
163 lastUserDragTime.referTo (transientState, IDs::lastUserDragTime, um);
164 reallocationInhibitors.referTo (transientState, IDs::reallocationInhibitors, um);
165 playbackContextAllocation.referTo (transientState, IDs::playbackContextAllocation, um);
166
167 rewindButtonDown.referTo (transientState, IDs::rewindButtonDown, um);
168 fastForwardButtonDown.referTo (transientState, IDs::fastForwardButtonDown, um);
169 nudgeLeftCount.referTo (transientState, IDs::nudgeLeftCount, um);
170 nudgeRightCount.referTo (transientState, IDs::nudgeRightCount, um);
171
172 videoPosition.referTo (transientState, IDs::videoPosition, um);
173 forceVideoJump.referTo (transientState, IDs::forceVideoJump, um);
174
175 // CachedValues need to be set so they aren't using their default values
176 // to avoid spurious listener callbacks
177 playing = playing.get();
178 recording = recording.get();
179 safeRecording = safeRecording.get();
180
181 state.addListener (this);
182 transientState.addListener (this);
183 }
184
187 {
188 jassert (reallocationInhibitors == 0);
189 }
190
192 void setVideoPosition (TimePosition time, bool forceJump)
193 {
194 forceVideoJump = forceJump;
195 videoPosition = time;
196 }
197
198 //==============================================================================
200 void play (bool justSendMMCIfEnabled_)
201 {
202 justSendMMCIfEnabled = justSendMMCIfEnabled_;
203 playing = true;
204 }
205
207 void record (bool justSendMMCIfEnabled_, bool allowRecordingIfNoInputsArmed_)
208 {
209 justSendMMCIfEnabled = justSendMMCIfEnabled_;
210 allowRecordingIfNoInputsArmed = allowRecordingIfNoInputsArmed_;
211 recording = true;
212 }
213
215 void stop (bool discardRecordings_,
216 bool clearDevices_,
217 bool canSendMMCStop_)
218 {
219 discardRecordings = discardRecordings_;
220 clearDevices = clearDevices_;
221 canSendMMCStop = canSendMMCStop_;
222 playing = false;
223 }
224
225 void updatePositionFromPlayhead (TimePosition newPosition)
226 {
227 updatingFromPlayHead = true;
228 state.setProperty (IDs::position, newPosition.inSeconds(), nullptr);
229 updatingFromPlayHead = false;
230 }
231
232 void nudgeLeft()
233 {
234 nudgeLeftCount = ((nudgeLeftCount + 1) % 2);
235 }
236
237 void nudgeRight()
238 {
239 nudgeRightCount = ((nudgeRightCount + 1) % 2);
240 }
241
242 //==============================================================================
243 juce::CachedValue<bool> playing, recording, safeRecording;
244 juce::CachedValue<bool> discardRecordings, clearDevices, justSendMMCIfEnabled, canSendMMCStop,
245 allowRecordingIfNoInputsArmed, clearDevicesOnStop;
246 juce::CachedValue<bool> userDragging, forceVideoJump, rewindButtonDown, fastForwardButtonDown, updatingFromPlayHead;
247 juce::CachedValue<juce::int64> lastUserDragTime;
248 juce::CachedValue<TimePosition> startTime, endTime;
250 juce::CachedValue<int> reallocationInhibitors, playbackContextAllocation, nudgeLeftCount, nudgeRightCount;
251
252 juce::ValueTree state, transientState { IDs::TRANSPORT };
253 TransportControl& transport;
254
255private:
256 bool isInsideRecordingCallback = false;
257
258 void valueTreePropertyChanged (juce::ValueTree& v, const juce::Identifier& i) override
259 {
260 if (v == state)
261 {
262 if (i == IDs::position)
263 {
264 if (! updatingFromPlayHead)
265 transport.performPositionChange();
266 }
267 else if (i == IDs::looping)
268 {
269 transport.stopIfRecording();
270
271 auto& ecm = transport.engine.getExternalControllerManager();
272
273 if (ecm.isAttachedToEdit (transport.edit))
274 ecm.loopChanged (state[IDs::looping]);
275 }
276 else if (i == IDs::snapToTimecode)
277 {
278 auto& ecm = transport.engine.getExternalControllerManager();
279
280 if (ecm.isAttachedToEdit (transport.edit))
281 ecm.snapChanged (state[IDs::snapToTimecode]);
282 }
283 }
284 else if (v == transientState)
285 {
286 if (i == IDs::playing)
287 {
288 playing.forceUpdateOfCachedValue();
289
290 if (playing)
291 transport.performPlay();
292 else
293 transport.performStop();
294
295 transport.startedOrStopped();
296 }
297 else if (i == IDs::recording)
298 {
299 // This recursion check is to avoid the call to performRecord stopping
300 // playback which in turn stops recording as it is trying to be started
301 if (isInsideRecordingCallback)
302 return;
303
304 recording.forceUpdateOfCachedValue();
305
306 if (recording)
307 {
308 juce::ScopedValueSetter<bool> svs (isInsideRecordingCallback, true);
309
310 if (auto res = transport.performRecord())
311 {
312 recording = true;
313 transport.listeners.call (&TransportControl::Listener::recordingStarted, res->first, res->second);
314 }
315 else
316 {
317 recording = false;
318 }
319 }
320 else
321 {
322 transport.performStopRecording();
323 }
324 }
325 else if (i == IDs::playbackContextAllocation)
326 {
327 transport.listeners.call (&TransportControl::Listener::playbackContextChanged);
328 }
329 else if (i == IDs::videoPosition)
330 {
331 videoPosition.forceUpdateOfCachedValue();
332 transport.listeners.call (&TransportControl::Listener::setVideoPosition, videoPosition.get(), forceVideoJump);
333 }
334 else if (i == IDs::rewindButtonDown)
335 {
336 fastForwardButtonDown = false;
337 rewindButtonDown.forceUpdateOfCachedValue();
338 transport.performRewindButtonChanged();
339 }
340 else if (i == IDs::fastForwardButtonDown)
341 {
342 rewindButtonDown = false;
343 fastForwardButtonDown.forceUpdateOfCachedValue();
344 transport.performFastForwardButtonChanged();
345 }
346 else if (i == IDs::nudgeLeftCount)
347 {
348 transport.performNudgeLeft();
349 }
350 else if (i == IDs::nudgeRightCount)
351 {
352 transport.performNudgeRight();
353 }
354 }
355 }
356};
357
358//==============================================================================
360{
361 SectionPlayer (TransportControl& tc, TimeRange sectionToPlay)
362 : transport (tc), section (sectionToPlay),
363 wasLooping (tc.looping)
364 {
365 jassert (! sectionToPlay.isEmpty());
366 transport.setPosition (sectionToPlay.getStart());
367 transport.looping = false;
368 transport.play (false);
369
370 startTimerHz (25);
371 }
372
373 ~SectionPlayer() override
374 {
375 if (wasLooping)
376 transport.looping = true;
377 }
378
379 TransportControl& transport;
380 const TimeRange section;
381 const bool wasLooping;
382
383 void timerCallback() override
384 {
385 if (transport.getPosition() > section.getEnd())
386 transport.stop (false, false); // Will delete the SectionPlayer
387 }
388};
389
390//==============================================================================
392{
394 : owner (tc)
395 {
396 startTimer (500);
397 }
398
399 void timerCallback() override
400 {
401 if (owner.edit.isLoading())
402 return;
403
405
406 if (active && forcePurge)
407 {
408 hasBeenDeactivated = true;
409 active = false;
410 }
411
412 auto canPurge = [this]
413 {
414 if (owner.isPlaying() || owner.isRecording())
415 return false;
416
417 return SmartThumbnail::areThumbnailsFullyLoaded (owner.engine);
418 };
419
420 if (active != hasBeenDeactivated
421 && canPurge())
422 {
423 hasBeenDeactivated = active;
424
425 if (! active)
426 {
427 if (! forcePurge)
428 owner.engine.getAudioFileManager().releaseAllFiles();
429
430 TemporaryFileManager::purgeOrphanFreezeAndProxyFiles (owner.edit);
431 forcePurge = false;
432 }
433 else
434 {
435 owner.engine.getAudioFileManager().checkFilesForChanges();
436 }
437 }
438 }
439
440 TransportControl& owner;
441 bool hasBeenDeactivated = false, forcePurge = false;
442};
443
444//==============================================================================
446{
447 ButtonRepeater (TransportControl& tc, bool isRW)
448 : owner (tc), isRewind (isRW)
449 {
450 }
451
452 void setDown (bool b)
453 {
454 accel = 1.0;
455 lastClickTime = juce::Time::getCurrentTime();
456
457 if (b != isDown)
458 {
459 isDown = b;
460
461 if (b)
462 {
463 firstPress = true;
464 buttonDownTime = juce::Time::getCurrentTime();
465 }
466
467 static int buttsDown = 0;
468
469 if (b)
470 {
471 ++buttsDown;
472 startTimer (20);
473 timerCallback();
474 }
475 else
476 {
477 --buttsDown;
478 stopTimer();
479 }
480
481 owner.setUserDragging (buttsDown > 0);
482 }
483 }
484
485 void nudge()
486 {
487 setDown (true);
488 timerCallback();
489 setDown (false);
490 }
491
492private:
493 TransportControl& owner;
494 double accel = 1.0;
495 bool isRewind, isDown = false, firstPress = false;
496 juce::Time buttonDownTime, lastClickTime;
497
498 void timerCallback() override
499 {
500 auto now = juce::Time::getCurrentTime();
501 double secs = (now - lastClickTime).inSeconds();
502 lastClickTime = now;
503
504 if (isRewind)
505 {
506 // don't respond to both keys at once
507 if (owner.ffRepeater->isDown)
508 return;
509
510 secs = -secs;
511 }
512
513 if (owner.snapToTimecode)
514 {
515 if ((juce::Time::getCurrentTime() - buttonDownTime).inSeconds() < 0.5)
516 {
517 if (firstPress)
518 {
519 firstPress = false;
520
521 auto t = owner.getPosition();
522
523 if (isRewind)
524 t = TransportHelpers::snapTimeDown (owner, t - 1.0e-5s, false);
525 else
526 t = TransportHelpers::snapTimeUp (owner, t + 1.0e-5s, false);
527
528 owner.setPosition (std::max (0_tp, t));
529 }
530
531 return;
532 }
533 }
534
535 secs *= accel;
536 accel = std::min (accel + 0.1, 6.0);
537
538 scrub (owner, secs * 10.0);
539 }
540
542};
543
544//==============================================================================
546{
548 : transport (t)
549 {}
550
551 tracktion::graph::PlayHead* getNodePlayHead() const
552 {
553 return transport.playbackContext ? transport.playbackContext->getNodePlayHead()
554 : nullptr;
555 }
556
557 double getSampleRate() const
558 {
559 return transport.playbackContext ? transport.playbackContext->getSampleRate()
560 : 44100.0;
561 }
562
563 void play()
564 {
565 if (auto ph = getNodePlayHead())
566 ph->play();
567 }
568
569 void play (TimeRange timeRange, bool looped)
570 {
571 if (auto ph = getNodePlayHead())
572 ph->play (tracktion::toSamples (timeRange, getSampleRate()), looped);
573 }
574
575 void setRollInToLoop (TimePosition prerollStartTime)
576 {
577 if (auto ph = getNodePlayHead())
578 ph->setRollInToLoop (tracktion::toSamples (prerollStartTime, getSampleRate()));
579 }
580
581 void stop()
582 {
583 if (auto ph = getNodePlayHead())
584 ph->stop();
585 }
586
587 bool isPlaying() const
588 {
589 if (auto ph = getNodePlayHead())
590 return ph->isPlaying();
591
592 return false;
593 }
594
597 {
598 if (getNodePlayHead() != nullptr && transport.playbackContext != nullptr && transport.playbackContext->isPlaybackGraphAllocated())
599 return transport.playbackContext->getAudibleTimelineTime();
600
601 return getPosition();
602 }
603
604 TimePosition getPosition() const
605 {
606 if (auto ph = getNodePlayHead())
607 return TimePosition::fromSamples (ph->getPosition(), getSampleRate());
608
609 return {};
610 }
611
612 TimePosition getUnloopedPosition() const
613 {
614 if (auto ph = getNodePlayHead())
615 return TimePosition::fromSamples (ph->getUnloopedPosition(), getSampleRate());
616
617 return {};
618 }
619
620 void setPosition (TimePosition newPos)
621 {
622 if (getNodePlayHead() != nullptr)
623 transport.playbackContext->postPosition (newPos);
624 }
625
626 bool isLooping() const
627 {
628 if (auto ph = getNodePlayHead())
629 return ph->isLooping();
630
631 return false;
632 }
633
634 TimeRange getLoopTimes() const
635 {
636 if (auto ph = getNodePlayHead())
637 return tracktion::timeRangeFromSamples (ph->getLoopRange(), getSampleRate());
638
639 return {};
640 }
641
642 void setLoopTimes (bool loop, TimeRange newRange)
643 {
644 if (auto ph = getNodePlayHead())
645 ph->setLoopRange (loop, tracktion::toSamples (newRange, getSampleRate()));
646 }
647
648 void setUserIsDragging (bool isDragging)
649 {
650 if (auto ph = getNodePlayHead())
651 ph->setUserIsDragging (isDragging);
652 }
653
654private:
655 TransportControl& transport;
656};
657
658
659//==============================================================================
660juce::Array<TransportControl*> TransportControl::getAllActiveTransports (Engine& engine)
661{
663
664 for (auto edit : engine.getActiveEdits().getEdits())
665 controls.add (&edit->getTransport());
666
667 return controls;
668}
669
670int TransportControl::getNumPlayingTransports (Engine& engine)
671{
672 return engine.getActiveEdits().numTransportsPlaying;
673}
674
675void TransportControl::stopAllTransports (Engine& engine, bool discardRecordings, bool clearDevices)
676{
677 for (auto tc : getAllActiveTransports (engine))
678 tc->stop (discardRecordings, clearDevices);
679}
680
681std::vector<std::unique_ptr<TransportControl::ScopedContextAllocator>> TransportControl::restartAllTransports (Engine& engine, bool clearDevices)
682{
684
685 for (auto tc : getAllActiveTransports (engine))
686 {
688
689 if (clearDevices)
690 {
692 tc->stop (false, true);
694 }
695 else
696 {
697 tc->stopIfRecording();
698 }
699
700 tc->edit.restartPlayback();
701 }
702
703 return restartHandles;
704}
705
706void TransportControl::callRecordingAboutToStartListeners (InputDeviceInstance& in, EditItemID targetID)
707{
708 listeners.call (&Listener::recordingAboutToStart, in, targetID);
709}
710
711void TransportControl::callRecordingAboutToStopListeners (InputDeviceInstance& in, EditItemID targetID)
712{
713 recordingIsStoppingFlag = true;
714 listeners.call (&Listener::recordingAboutToStop, in, targetID);
715}
716
717void TransportControl::callRecordingFinishedListeners (InputDeviceInstance& in, EditItemID targetID, Clip::Array recordedClips)
718{
719 recordingIsStoppingFlag = false;
720 listeners.call (&Listener::recordingFinished, in, targetID, recordedClips);
721}
722
723TransportControl::PlayingFlag::PlayingFlag (Engine& e) noexcept : engine (e) { ++engine.getActiveEdits().numTransportsPlaying; }
724TransportControl::PlayingFlag::~PlayingFlag() noexcept { --engine.getActiveEdits().numTransportsPlaying; }
725
726//==============================================================================
727void TransportControl::editHasChanged()
728{
729 if (transportState->reallocationInhibitors > 0)
730 {
731 isDelayedChangePending = true;
732 return;
733 }
734
735 isDelayedChangePending = false;
736
737 if (playbackContext == nullptr)
738 return;
739
741 engine.getExternalControllerManager().updateAllDevices();
742}
743
744bool TransportControl::isAllowedToReallocate() const noexcept
745{
746 return transportState->reallocationInhibitors <= 0;
747}
748
749//==============================================================================
751 : transport (tc)
752{
753 auto& inhibitors = transport.transportState->reallocationInhibitors;
754 inhibitors = inhibitors + 1;
755}
756
758{
759 auto& inhibitors = transport.transportState->reallocationInhibitors;
760 jassert (inhibitors > 0);
761 inhibitors = std::max (0, inhibitors - 1);
762}
763
764
765//==============================================================================
766void TransportControl::releaseAudioNodes()
767{
768 if (playbackContext != nullptr)
769 playbackContext->clearNodes();
770}
771
773{
774 if (! edit.shouldPlay())
775 return;
776
777 const auto start = position.get();
778
779 if (playbackContext == nullptr)
780 {
781 playbackContext = std::make_unique<EditPlaybackContext> (*this);
782 playbackContext->createPlayAudioNodes (start);
783 transportState->playbackContextAllocation = transportState->playbackContextAllocation + 1;
784 }
785
786 if (alwaysReallocate)
787 playbackContext->createPlayAudioNodes (start);
788 else
789 playbackContext->createPlayAudioNodesIfNeeded (start);
790}
791
793{
794 playbackContext.reset();
795 clearPlayingFlags();
796 transportState->playbackContextAllocation = std::max (0, transportState->playbackContextAllocation - 1);
797}
798
800{
801 transportState->clearDevicesOnStop = true;
802
803 if (isPlaying() || edit.isRendering())
804 return;
805
806 stop (false, true);
808}
809
811{
812 fileFlushTimer->forcePurge = true;
813}
814
815//==============================================================================
816static int numScreenSaverDefeaters = 0;
817
819{
821 {
822 if (juce::Desktop::getInstance().isHeadless())
823 return;
824
825 TRACKTION_ASSERT_MESSAGE_THREAD
826 ++numScreenSaverDefeaters;
827 juce::Desktop::setScreenSaverEnabled (numScreenSaverDefeaters == 0);
828 }
829
831 {
832 if (juce::Desktop::getInstance().isHeadless())
833 return;
834
835 TRACKTION_ASSERT_MESSAGE_THREAD
836 --numScreenSaverDefeaters;
837 jassert (numScreenSaverDefeaters >= 0);
838 juce::Desktop::setScreenSaverEnabled (numScreenSaverDefeaters == 0);
839 }
840};
841
842//==============================================================================
843static juce::Array<TransportControl*, juce::CriticalSection> activeTransportControls;
844
845//==============================================================================
847 : engine (ed.engine), edit (ed), state (v)
848{
849 jassert (state.hasType (IDs::TRANSPORT));
850 juce::UndoManager* um = nullptr;
851 startPosition.referTo (state, IDs::start, um);
852 position.referTo (state, IDs::position, um);
853 loopPoint1.referTo (state, IDs::loopPoint1, um);
854 loopPoint2.referTo (state, IDs::loopPoint2, um);
855 snapToTimecode.referTo (state, IDs::snapToTimecode, um, true);
856 looping.referTo (state, IDs::looping, um);
857 scrubInterval.referTo (state, IDs::scrubInterval, um, 0.1s);
858
859 playHeadWrapper = std::make_unique<PlayHeadWrapper> (*this);
860 transportState = std::make_unique<TransportState> (*this, state);
861
862 rwRepeater = std::make_unique<ButtonRepeater> (*this, true);
863 ffRepeater = std::make_unique<ButtonRepeater> (*this, false);
864
865 fileFlushTimer = std::make_unique<FileFlushTimer> (*this);
866
867 activeTransportControls.add (this);
868 startTimerHz (50);
869}
870
872{
873 activeTransportControls.removeAllInstancesOf (this);
874 fileFlushTimer = nullptr;
875
877 stop (false, true);
878}
879
880//==============================================================================
881void TransportControl::play (bool justSendMMCIfEnabled)
882{
883 transportState->play (justSendMMCIfEnabled);
884}
885
886void TransportControl::playFromStart (bool justSendMMCIfEnabled)
887{
889 TransportHelpers::resyncLauncherClips (*this, {}, true);
890 play (justSendMMCIfEnabled);
891}
892
893void TransportControl::playSectionAndReset (TimeRange rangeToPlay)
894{
896
897 if (! isPlaying())
898 sectionPlayer = std::make_unique<SectionPlayer> (*this, rangeToPlay);
899}
900
901void TransportControl::record (bool justSendMMCIfEnabled, bool allowRecordingIfNoInputsArmed)
902{
903 transportState->record (justSendMMCIfEnabled, allowRecordingIfNoInputsArmed);
904}
905
906void TransportControl::stop (bool discardRecordings,
907 bool clearDevices,
908 bool canSendMMCStop)
909{
910 transportState->stop (discardRecordings,
911 clearDevices,
912 canSendMMCStop);
913}
914
916{
917 if (isRecording())
918 stop (false, false);
919}
920
921void TransportControl::stopRecording (bool discardRecordings)
922{
923 if (! isRecording())
924 return;
925
926 transportState->discardRecordings = discardRecordings;
927 transportState->recording = false;
928}
929
931{
932 if (static_cast<int> (engine.getPropertyStorage().getProperty (SettingID::retrospectiveRecord, 30)) == 0)
933 return juce::Result::fail (TRANS("Retrospective record is currently disabled"));
934
935 if (playbackContext)
936 {
937 juce::Array<Clip*> clips;
938 return playbackContext->applyRetrospectiveRecord (&clips, armedOnly);
939 }
940
941 return juce::Result::fail (TRANS("No active audio devices"));
942}
943
945{
946 if (static_cast<int> (engine.getPropertyStorage().getProperty (SettingID::retrospectiveRecord, 30)) == 0)
947 return {};
948
949 if (playbackContext)
950 {
952 juce::Array<Clip*> clips;
953 playbackContext->applyRetrospectiveRecord (&clips);
954
955 if (clips.size() > 0)
956 {
957 for (auto c : clips)
958 {
959 if (auto ac = dynamic_cast<WaveAudioClip*> (c))
960 {
961 auto f = ac->getOriginalFile();
962 files.add (f);
963 }
964 else if (auto mc = dynamic_cast<MidiClip*> (c))
965 {
966 auto clipPos = mc->getPosition();
967
968 juce::Array<Clip*> clipsToRender;
969 clipsToRender.add (mc);
970
972
973 auto f = dir.getNonexistentChildFile (juce::File::createLegalFileName (mc->getName()), ".wav");
974
975 juce::BigInteger tracksToDo;
976 int idx = 0;
977
978 for (auto t : getAllTracks (edit))
979 {
980 if (mc->getTrack() == t)
981 tracksToDo.setBit (idx);
982
983 idx++;
984 }
985
986 Renderer::renderToFile (TRANS("Render Clip"), f, edit, clipPos.time,
987 tracksToDo, true, false, clipsToRender, true);
988
989 files.add (f);
990 }
991
992 c->removeFromParent();
993 }
994 return files;
995 }
996 }
997
998 return {};
999}
1000
1001void TransportControl::syncToEdit (Edit* editToSyncTo, bool isPreview)
1002{
1004
1005 if (playbackContext && editToSyncTo != nullptr)
1006 {
1007 if (auto targetContext = editToSyncTo->getTransport().getCurrentPlaybackContext())
1008 {
1009 auto& tempoSequence = editToSyncTo->tempoSequence;
1010 auto& tempo = tempoSequence.getTempoAt (position);
1011 auto& timeSig = tempoSequence.getTimeSigAt (position);
1012
1013 auto barsBeats = tempoSequence.toBarsAndBeats (targetContext->isLooping()
1014 ? targetContext->getLoopTimes().getStart()
1015 : position);
1016
1017 auto previousBarTime = tempoSequence.toTime ({ barsBeats.bars, {} });
1018
1019 auto syncInterval = isPreview ? targetContext->getLoopTimes().getLength()
1020 : TimeDuration::fromSeconds ((60.0 / tempo.getBpm() * timeSig.numerator));
1021
1022 playbackContext->syncToContext (targetContext, previousBarTime, syncInterval);
1023 }
1024 }
1025}
1026
1027bool TransportControl::isPlaying() const { return transportState->playing; }
1028bool TransportControl::isRecording() const { return transportState->recording; }
1029bool TransportControl::isSafeRecording() const { return isRecording() && transportState->safeRecording; }
1030bool TransportControl::isStopping() const { return isStopInProgress; }
1031bool TransportControl::isRecordingStopping() const { return recordingIsStoppingFlag; }
1032
1033
1034TimePosition TransportControl::getTimeWhenStarted() const { return transportState->startTime.get(); }
1035
1036//==============================================================================
1037bool TransportControl::areAnyInputsRecording()
1038{
1039 for (auto in : edit.getAllInputDevices())
1040 if (in->isRecordingActive())
1041 return true;
1042
1043 return false;
1044}
1045
1046void TransportControl::clearPlayingFlags()
1047{
1048 transportState->playing = false;
1049 transportState->recording = false;
1050 transportState->safeRecording = false;
1051 playingFlag.reset();
1052}
1053
1054//==============================================================================
1055void TransportControl::timerCallback()
1056{
1058
1059 if (playbackContext == nullptr)
1060 return;
1061
1062 if (isDelayedChangePending)
1064
1065 if (isPlaying() && playHeadWrapper->getPosition() >= Edit::getMaximumEditEnd())
1066 {
1067 stop (false, false);
1068 position = Edit::getMaximumEditEnd();
1069 return;
1070 }
1071
1072 if (! playHeadWrapper->isPlaying())
1073 {
1074 if (isRecording())
1075 {
1076 stop (false, false);
1077 return;
1078 }
1079
1080 if (isPlaying())
1081 stop (false, false);
1082 }
1083 else if (! isPlaying())
1084 {
1085 // Playhead is playing but transport state is stopped so start playing
1086 play (false);
1087 }
1088
1089 // Update the transport state from the playhead if we have one
1091 && (! transportState->userDragging)
1092 && juce::Time::getMillisecondCounter() - transportState->lastUserDragTime > 200)
1093 {
1094 // Only update if we're not looping or we're playing as otherwise the transport
1095 // position will jump and be stuck at the loop in position.
1096 // The other way to fix this mught be to change the play head to only snap the position on play start..
1097 if (! looping || isPlaying())
1098 {
1099 const auto currentTime = playHeadWrapper->getLiveTransportPosition();
1100 transportState->setVideoPosition (currentTime, false);
1101 transportState->updatePositionFromPlayhead (currentTime);
1102 }
1103 }
1104
1105 // Periodically update the loop times from the transport state
1106 if (--loopUpdateCounter == 0)
1107 {
1108 loopUpdateCounter = 10;
1109
1110 if (looping)
1111 {
1112 auto lr = getLoopRange();
1113 lr = lr.withEnd (std::max (lr.getEnd(), lr.getStart() + 0.001s));
1114 playHeadWrapper->setLoopTimes (true, lr);
1115 }
1116 else
1117 {
1118 playHeadWrapper->setLoopTimes (false, {});
1119 }
1120 }
1121}
1122
1123
1124//==============================================================================
1126{
1127 sectionPlayer.reset();
1128 transportState->rewindButtonDown = isDown;
1129}
1130
1132{
1133 sectionPlayer.reset();
1134 transportState->fastForwardButtonDown = isDown;
1135}
1136
1138{
1139 sectionPlayer.reset();
1140 transportState->nudgeLeft();
1141}
1142
1144{
1145 sectionPlayer.reset();
1146 transportState->nudgeRight();
1147}
1148
1149
1150//==============================================================================
1152{
1153 return position.get();
1154}
1155
1157{
1158 // This drag time update is here to avoid the transport position being updated
1159 // from the playhead before the position has a chance to be dispatched by it
1160 transportState->lastUserDragTime = juce::Time::getMillisecondCounter();
1161 position = t;
1162}
1163
1164void TransportControl::setPosition (TimePosition timeToMoveTo, TimePosition timeToPerformJump)
1165{
1166 if (auto epc = getCurrentPlaybackContext())
1167 epc->postPosition (timeToMoveTo, timeToPerformJump);
1168
1170}
1171
1173{
1175
1176 if (playbackContext != nullptr)
1177 playHeadWrapper->setUserIsDragging (b);
1178
1179 if (b != transportState->userDragging)
1180 {
1181 if (transportState->userDragging && isPlaying())
1182 {
1183 edit.getAutomationRecordManager().punchOut (false);
1184
1185 if (playbackContext != nullptr)
1186 playHeadWrapper->setPosition (position);
1187 }
1188
1189 transportState->userDragging = b;
1190
1191 if (b)
1192 transportState->lastUserDragTime = juce::Time::getMillisecondCounter();
1193 }
1194}
1195
1197{
1198 return transportState->userDragging;
1199}
1200
1202{
1203 return transportState->updatingFromPlayHead;
1204}
1205
1206//==============================================================================
1208{
1209 setLoopPoint1 (std::max (std::max (loopPoint1.get(), loopPoint2.get()), std::max (TimePosition(), t)));
1211}
1212
1214{
1215 setLoopPoint1 (std::min (std::min (loopPoint1.get(), loopPoint2.get()), std::max (TimePosition(), t)));
1217}
1218
1220{
1221 loopPoint1 = juce::jlimit (0_tp, toPosition (edit.getLength() + Edit::getMaximumLength() * 0.75), t);
1222}
1223
1225{
1226 loopPoint2 = juce::jlimit (0_tp, toPosition (edit.getLength() + Edit::getMaximumLength() * 0.75), t);
1227}
1228
1230{
1231 auto maxEndTime = toPosition (edit.getLength() + Edit::getMaximumLength() * 0.75);
1232
1233 loopPoint1 = juce::jlimit (0_tp, maxEndTime, times.getStart());
1234 loopPoint2 = juce::jlimit (0_tp, maxEndTime, times.getEnd());
1235}
1236
1237TimeRange TransportControl::getLoopRange() const noexcept
1238{
1239 return TimeRange::between (loopPoint1, loopPoint2);
1240}
1241
1243{
1244 currentSnapType = newSnapType;
1245}
1246
1247
1248//==============================================================================
1249void TransportControl::startedOrStopped()
1250{
1251 if (lastPlayStatus != isPlaying() || lastRecordStatus != isRecording())
1252 {
1253 const bool wasRecording = lastRecordStatus;
1254
1255 {
1258
1259 lastPlayStatus = isPlaying();
1260 lastRecordStatus = isRecording();
1261
1263 }
1264
1265 {
1267 if (isPlaying())
1268 {
1269 transportState->setVideoPosition (getPosition(), true);
1270 listeners.call (&Listener::startVideo);
1271
1272 if (wasRecording)
1273 listeners.call (&Listener::autoSaveNow);
1274 }
1275 else
1276 {
1277 listeners.call (&Listener::stopVideo);
1278 }
1279
1280 listeners.call (&Listener::setAllLevelMetersActive, false);
1281 listeners.call (&Listener::setAllLevelMetersActive, true);
1282 }
1283 }
1284}
1285
1286void TransportControl::sendMMC (const juce::MidiMessage& mmc)
1287{
1289 auto& dm = engine.getDeviceManager();
1290
1291 for (int i = dm.getNumMidiOutDevices(); --i >= 0;)
1292 {
1293 if (auto* mo = dm.getMidiOutDevice (i))
1294 {
1295 if (mo->isEnabled() && mo->isSendingMMC())
1296 {
1297 mo->fireMessage (mmc);
1298 break;
1299 }
1300 }
1301 }
1302}
1303
1304void TransportControl::sendMMCCommand (juce::MidiMessage::MidiMachineControlCommand command)
1305{
1307}
1308
1309inline bool anyEnabledMidiOutDevicesSendingMMC (DeviceManager& dm)
1310{
1311 for (int i = dm.getNumMidiOutDevices(); --i >= 0;)
1312 if (auto mo = dm.getMidiOutDevice (i))
1313 if (mo->isEnabled() && mo->isSendingMMC())
1314 return true;
1315
1316 return false;
1317}
1318
1319bool TransportControl::sendMMCStartPlay()
1320{
1321 if (anyEnabledMidiOutDevicesSendingMMC (engine.getDeviceManager()))
1322 {
1323 sendMMCCommand (juce::MidiMessage::mmc_play);
1324
1326 return true;
1327 }
1328
1329 return false;
1330}
1331
1332bool TransportControl::sendMMCStartRecord()
1333{
1334 if (anyEnabledMidiOutDevicesSendingMMC (engine.getDeviceManager()))
1335 {
1336 sendMMCCommand (juce::MidiMessage::mmc_recordStart);
1337
1339 return true;
1340 }
1341
1342 return false;
1343}
1344
1345//==============================================================================
1346void TransportControl::performPlay()
1347{
1349 sectionPlayer.reset();
1350
1351 if (! edit.shouldPlay())
1352 return;
1353
1354 if (! playingFlag)
1355 {
1356 if (transportState->justSendMMCIfEnabled && sendMMCStartPlay())
1357 return;
1358
1359 if (looping)
1360 {
1361 const auto cursorPos = position.get();
1362 const auto loopRange = getLoopRange();
1363
1364 if (cursorPos < loopRange.getStart()
1365 || cursorPos > loopRange.getEnd() - 0.1s)
1366 {
1367 position = loopRange.getStart();
1368 }
1369
1370 transportState->startTime = loopRange.getStart();
1371 transportState->endTime = loopRange.getEnd();
1372
1373 if (transportState->endTime < transportState->startTime + 0.01s)
1374 {
1375 engine.getUIBehaviour().showWarningMessage (TRANS("Can't play in loop mode unless the in/out markers are further apart"));
1376 return;
1377 }
1378 }
1379 else
1380 {
1381 transportState->startTime = position.get();
1382 transportState->endTime = Edit::getMaximumEditEnd();
1383 }
1384
1386 {
1387 const double barLength = edit.tempoSequence.getTimeSig(0)->numerator;
1388 const double beatsUntilNextLinkCycle = edit.getAbletonLink().getBeatsUntilNextCycle (barLength);
1389
1390 const double cyclePos = std::fmod (transportState->startTime.get().inSeconds(), barLength);
1391 const double nextLinkCycle = edit.tempoSequence.toTime (BeatPosition::fromBeats (beatsUntilNextLinkCycle)).inSeconds();
1392
1393 transportState->startTime = TimePosition::fromSeconds ((transportState->startTime.get().inSeconds() - cyclePos) + (barLength - nextLinkCycle));
1394 }
1395
1396 transportState->recording = false;
1397 transportState->safeRecording = false;
1398 playingFlag = std::make_unique<PlayingFlag> (engine);
1399
1401
1402 if (playbackContext)
1403 {
1404 playHeadWrapper->play ({ transportState->startTime, transportState->endTime }, looping);
1405
1406 // Post the position change to be dispatched otherwise what we're effectively doing is setting
1407 // the position for "this" block and it will get incremented the next block, actually starting
1408 // playback 1 block from the start
1409 playHeadWrapper->setPosition (position);
1410 }
1411 else
1412 {
1413 clearPlayingFlags();
1414 }
1415
1417 }
1418}
1419
1420std::optional<std::pair<SyncPoint, std::optional<TimeRange>>> TransportControl::performRecord()
1421{
1422 if (! edit.shouldPlay())
1423 return std::nullopt;
1424
1426 sectionPlayer.reset();
1427 std::optional<SyncPoint> punchInPoint;
1428 std::optional<TimeRange> punchRange;
1429
1430 if (! transportState->userDragging)
1431 {
1432 if (transportState->justSendMMCIfEnabled && sendMMCStartRecord())
1433 return std::nullopt;
1434
1435 if (transportState->allowRecordingIfNoInputsArmed || areAnyInputsRecording())
1436 {
1437 // If we're already playing, just start the armed inputs recording and enable the click track
1438 if (isPlaying())
1439 {
1440 assert (playbackContext);
1441
1442 punchInPoint = playbackContext->getSyncPoint();
1443
1445 punchRange = getLoopRange();
1446
1447 const auto currentPos = playbackContext->getPosition();
1448 const auto punchInTime = edit.recordingPunchInOut ? getLoopRange().getStart() : currentPos;
1449
1450 playbackContext->prepareForRecording (currentPos, punchInTime);
1451 transportState->safeRecording = engine.getPropertyStorage().getProperty (SettingID::safeRecord, false);
1452
1454 }
1455 else
1456 {
1457 const auto loopRange = getLoopRange();
1458 transportState->startTime = position.get();
1459 transportState->endTime = Edit::getMaximumEditEnd();
1460
1461 if (looping)
1462 {
1463 if (loopRange.getLength() < 2s)
1464 {
1465 engine.getUIBehaviour().showWarningMessage (TRANS("To record in loop mode, the length of loop must be greater than 2 seconds."));
1466 return std::nullopt;
1467 }
1468
1470 {
1471 engine.getUIBehaviour().showWarningMessage (TRANS("Recording can be done in either loop mode or punch in/out mode, but not both at the same time!"));
1472 return std::nullopt;
1473 }
1474
1475 transportState->startTime = loopRange.getStart();
1476 }
1477 else if (edit.recordingPunchInOut)
1478 {
1479 if ((loopRange.getEnd() + 0.1s) <= transportState->startTime)
1480 transportState->startTime = (loopRange.getStart() - 1.0s);
1481 }
1482 else
1483 {
1484 if (abs (transportState->startTime) < 0.005s)
1485 transportState->startTime = 0s;
1486 }
1487
1488 auto prerollStart = transportState->startTime.get();
1489 double numCountInBeats = edit.getNumCountInBeats();
1490 const auto& ts = edit.tempoSequence;
1491
1492 if (numCountInBeats > 0)
1493 {
1494 auto currentBeat = ts.toBeats (transportState->startTime);
1495 prerollStart = ts.toTime (currentBeat - BeatDuration::fromBeats (numCountInBeats + 0.5));
1496 // N.B. this +0.5 beats here specifies the behaviour further down when setting the click range.
1497 // If this changes, that will also need to change.
1498 }
1499
1501 {
1502 double barLength = ts.getTimeSig (0)->numerator;
1503 double beatsUntilNextLinkCycle = edit.getAbletonLink().getBeatsUntilNextCycle (barLength);
1504
1505 if (numCountInBeats > 0)
1506 beatsUntilNextLinkCycle -= 0.5;
1507
1508 prerollStart = prerollStart - toDuration (ts.toTime (BeatPosition::fromBeats (beatsUntilNextLinkCycle)));
1509 }
1510
1511 playingFlag = std::make_unique<PlayingFlag> (engine);
1512 transportState->safeRecording = engine.getPropertyStorage().getProperty (SettingID::safeRecord, false);
1513
1515
1517
1518 if (playbackContext)
1519 {
1520 if (edit.getNumCountInBeats() > 0)
1521 playHeadWrapper->setLoopTimes (true, { transportState->startTime.get(), Edit::getMaximumEditEnd() });
1522
1523 // if we're playing from near time = 0, roll back a fraction so we
1524 // don't miss the first block - this won't be noticable further along
1525 // in the edit.
1526 if (prerollStart < 0.2s)
1527 prerollStart = prerollStart - 0.2s;
1528
1529 if (looping)
1530 {
1531 // The order of this is critical as the audio thread might jump in and reset the
1532 // roll-in-to-loop status of the loop-range is not set first
1533 auto lr = getLoopRange();
1534 lr = lr.withEnd (std::max (lr.getEnd(), lr.getStart() + 0.001s));
1535 playHeadWrapper->setLoopTimes (true, lr);
1536 playHeadWrapper->setRollInToLoop (prerollStart);
1537 }
1538 else
1539 {
1540 // Set the playhead loop times before preparing the context as this will be used by
1541 // the RecordingContext to initialise itself
1542 playHeadWrapper->setLoopTimes (false, { prerollStart, transportState->endTime.get() });
1543 }
1544
1545 playHeadWrapper->setPosition (prerollStart);
1546 position = prerollStart;
1547
1548 // Prepare the recordings after the playhead has been setup to avoid synchronisation problems
1549 {
1550 playbackContext->blockUntilSyncPointChange();
1551
1553 punchRange = getLoopRange();
1554
1555 const auto currentSyncPoint = playbackContext->getSyncPoint();
1556 const auto punchInTime = transportState->startTime.get();
1557 const auto punchInBeat = ts.toBeats (punchInTime);
1558 const auto timeNow = currentSyncPoint->time;
1559 const auto beatNow = ts.toBeats (timeNow);
1560 const auto beatsUntilPunchIn = punchInBeat - beatNow;
1561 const auto samplesUntilPunchIn = toSamples (punchInTime - timeNow, playbackContext->getSampleRate());
1562 punchInPoint = SyncPoint { .referenceSamplePosition = currentSyncPoint->referenceSamplePosition + samplesUntilPunchIn,
1563 .monotonicBeat = { currentSyncPoint->monotonicBeat.v + beatsUntilPunchIn },
1564 .unloopedTime = punchInTime,
1565 .time = transportState->startTime.get(),
1566 .beat = ts.toBeats (transportState->startTime.get()) } ;
1567 jassert (juce::approximatelyEqual (currentSyncPoint->time.inSeconds(), currentSyncPoint->unloopedTime.inSeconds()));
1568
1569 TransportHelpers::resyncLauncherClips (*this, punchInPoint, false);
1570 }
1571
1572 playbackContext->prepareForRecording (prerollStart, transportState->startTime.get());
1573
1574 if (edit.getNumCountInBeats() > 0)
1575 {
1576 // As the pre-roll will be "num count in beats - 0.5" we have to add that back on before our calculation
1577 // We also roll back 0.5 beats the end time to avoid hearing a block that starts directly or just before a beat
1578 const auto clickStartBeat = ts.toBeats (prerollStart);
1579 const auto clickEndBeat = ts.toBeats (transportState->startTime.get());
1580
1581 edit.setClickTrackRange (ts.toTime ({ BeatPosition::fromBeats (std::ceil (clickStartBeat.inBeats() + 0.5)),
1582 BeatPosition::fromBeats (std::ceil (clickEndBeat.inBeats())) - 0.5_bd }));
1583 }
1584 else
1585 {
1587 }
1588
1589 playHeadWrapper->play();
1590 transportState->playing = true; // N.B. set these after the devices have been rebuilt and the playingFlag has been set
1591 screenSaverDefeater = std::make_unique<ScreenSaverDefeater>();
1592 }
1593 }
1594 }
1595 else
1596 {
1598 TRANS("Recording is only possible when at least one active input device is assigned to a track"));
1599
1600 return std::nullopt;
1601 }
1602 }
1603
1604 if (! transportState->justSendMMCIfEnabled)
1605 sendMMCCommand (juce::MidiMessage::mmc_recordStart);
1606
1607 if (transportState->safeRecording)
1608 engine.getUIBehaviour().showSafeRecordDialog (*this);
1609
1610 assert (punchInPoint);
1611 return std::make_pair (*punchInPoint, punchRange);
1612}
1613
1614std::optional<SyncPoint> TransportControl::performStopRecording()
1615{
1616 if (! playbackContext)
1617 return std::nullopt;
1618
1619 // This "! isRecording()" is backwards as it's in response to the recording state being turned off
1620 // This is messy and will be cleaned up soon
1621 if (! isRecording() || tracktion::isRecording (*playbackContext))
1622 {
1624
1625 const bool discardRecordings = transportState->discardRecordings;
1626 const auto syncPoint = playbackContext->getSyncPoint();
1627 assert (syncPoint);
1628 playbackContext->stopRecording (syncPoint->unloopedTime, discardRecordings)
1629 .map_error ([this] (auto err) { engine.getUIBehaviour().showWarningAlert (TRANS("Recording"), err); });
1630
1632 listeners.call (&TransportControl::Listener::recordingStopped, *syncPoint, discardRecordings);
1633
1634 return syncPoint;
1635 }
1636
1637 return std::nullopt;
1638}
1639
1640void TransportControl::performStop()
1641{
1643
1644 const juce::ScopedValueSetter<bool> svs (isStopInProgress, true);
1645 screenSaverDefeater.reset();
1646 sectionPlayer.reset();
1647
1648 engine.getUIBehaviour().hideSafeRecordDialog (*this);
1649
1650 if (playbackContext == nullptr)
1651 {
1652 jassert (! (isPlaying() || isRecording()));
1653 clearPlayingFlags();
1654 return;
1655 }
1656
1657 if (! juce::Component::isMouseButtonDownAnywhere())
1658 setUserDragging (false); // in case it gets stuck
1659
1660 if (isRecording() || tracktion::isRecording (*playbackContext))
1661 {
1663
1664 // grab this before stopping the playhead
1665 auto recEndTime = playHeadWrapper->getUnloopedPosition();
1666 auto recEndPos = playHeadWrapper->getPosition();
1667
1668 clearPlayingFlags();
1669 playHeadWrapper->stop();
1670 auto syncPoint = playbackContext->getSyncPoint();
1671 assert (syncPoint);
1672 playbackContext->stopRecording (recEndTime, transportState->discardRecordings)
1673 .map_error ([this] (auto err) { engine.getUIBehaviour().showWarningAlert (TRANS("Recording"), err); });
1674
1675 position = transportState->discardRecordings ? transportState->startTime.get()
1676 : (looping ? recEndPos
1677 : recEndTime);
1678
1679 listeners.call (&TransportControl::Listener::recordingStopped, *syncPoint, transportState->discardRecordings);
1680 }
1681 else
1682 {
1683 if (transportState->discardRecordings)
1684 engine.getUIBehaviour().showWarningMessage (TRANS("Can only abort a recording when something's actually recording."));
1685
1686 clearPlayingFlags();
1687 playHeadWrapper->stop();
1688 }
1689
1690 if (transportState->clearDevices || ! edit.playInStopEnabled || transportState->clearDevicesOnStop)
1691 releaseAudioNodes();
1692 else
1694
1695 transportState->clearDevicesOnStop = false;
1696
1697 if (transportState->canSendMMCStop)
1698 sendMMCCommand (juce::MidiMessage::mmc_stop);
1699}
1700
1701void TransportControl::performPositionChange()
1702{
1704
1705 sectionPlayer.reset();
1706 edit.getAutomationRecordManager().punchOut (false);
1707
1708 if (isRecording())
1709 stop (false, false);
1710
1711 auto newPos = TimePosition::fromSeconds (static_cast<double> (state[IDs::position]));
1712
1713 if (isPlaying() && looping)
1714 {
1715 auto range = getLoopRange();
1716 newPos = juce::jlimit (range.getStart(), range.getEnd(), newPos);
1717 }
1718 else
1719 {
1720 const auto minStartTime = edit.tempoSequence.toTime (-BeatPosition::fromBeats (edit.getNumCountInBeats())) - 0.5s;
1721 newPos = juce::jlimit (minStartTime, Edit::getMaximumEditEnd(), newPos);
1722 }
1723
1724 if (playbackContext != nullptr)
1725 playHeadWrapper->setPosition (newPos);
1726
1727 position = newPos;
1728
1729 yieldGUIThread();
1730
1731 if (! transportState->userDragging)
1732 transportState->lastUserDragTime = juce::Time::getMillisecondCounter();
1733
1734 transportState->setVideoPosition (newPos, true);
1735
1736 // MMC
1737 const double nudge = 0.05 / 96000.0;
1738 const double mmcTime = std::max (0_tp, newPos + edit.getTimecodeOffset()).inSeconds() + nudge;
1739 const int framesPerSecond = edit.getTimecodeFormat().getFPS();
1740 const int frames = ((int) (mmcTime * framesPerSecond)) % framesPerSecond;
1741 const int hours = (int) (mmcTime * (1.0 / 3600.0));
1742 const int minutes = (((int) mmcTime) / 60) % 60;
1743 const int seconds = (((int) mmcTime) % 60);
1744
1745 sendMMC (juce::MidiMessage::midiMachineControlGoto (hours, minutes, seconds, frames));
1746}
1747
1748void TransportControl::performRewindButtonChanged()
1749{
1750 const bool isDown = transportState->rewindButtonDown;
1751 rwRepeater->setDown (isDown);
1752
1753 if (isDown)
1754 sendMMCCommand (juce::MidiMessage::mmc_rewind);
1755 else
1756 sendMMCCommand (isPlaying() ? juce::MidiMessage::mmc_play
1757 : juce::MidiMessage::mmc_stop);
1758}
1759
1760void TransportControl::performFastForwardButtonChanged()
1761{
1762 const bool isDown = transportState->fastForwardButtonDown;
1763 ffRepeater->setDown (isDown);
1764
1765 if (isDown)
1766 sendMMCCommand (juce::MidiMessage::mmc_fastforward);
1767 else
1768 sendMMCCommand (isPlaying() ? juce::MidiMessage::mmc_play
1769 : juce::MidiMessage::mmc_stop);
1770}
1771
1772void TransportControl::performNudgeLeft()
1773{
1774 rwRepeater->nudge();
1775}
1776
1777void TransportControl::performNudgeRight()
1778{
1779 ffRepeater->nudge();
1780}
1781
1782
1783//==============================================================================
1784static TimeRange getLimitsOfSelectedClips (Edit& edit, const SelectableList& items)
1785{
1786 auto range = getTimeRangeForSelectedItems (items);
1787
1788 if (range.isEmpty())
1789 return { {}, edit.getLength() };
1790
1791 return range;
1792}
1793
1795{
1796 auto selectionStart = getLimitsOfSelectedClips (tc.edit, items).getStart();
1797 tc.setPosition (tc.getPosition() < selectionStart + 0.001s ? 0_tp : selectionStart);
1798}
1799
1800void toEnd (TransportControl& tc, const SelectableList& items)
1801{
1802 auto selectionEnd = getLimitsOfSelectedClips (tc.edit, items).getEnd();
1803 tc.setPosition (tc.getPosition() > selectionEnd - 0.001s ? toPosition (tc.edit.getLength()) : selectionEnd);
1804}
1805
1808
1811
1812void scrub (TransportControl& tc, double units)
1813{
1815 const auto unitSize = tc.scrubInterval.get();
1816 auto timeToMove = unitSize * units;
1817 auto t = tc.getPosition() + timeToMove;
1818
1819 if (tc.snapToTimecode)
1820 {
1821 if (timeToMove > 0s)
1822 t = TransportHelpers::snapTimeUp (tc, t, false);
1823 else
1824 t = TransportHelpers::snapTimeDown (tc, t, false);
1825 }
1826
1827 if (tc.isUserDragging() && tc.engine.getPropertyStorage().getProperty (SettingID::snapCursor, false))
1828 t = TransportHelpers::snapTimeDown (tc, t, false);
1829
1830 tc.setPosition (std::max (0_tp, t));
1831}
1832
1838
1839}} // namespace tracktion { inline namespace engine
assert
bool isEmpty() const noexcept
int size() const noexcept
void add(const ElementType &newElement)
BigInteger & setBit(int bitNumber)
void referTo(ValueTree &tree, const Identifier &property, UndoManager *um)
static Desktop &JUCE_CALLTYPE getInstance()
static void setScreenSaverEnabled(bool isEnabled)
static File JUCE_CALLTYPE getSpecialLocation(const SpecialLocationType type)
static String createLegalFileName(const String &fileNameToFix)
static MidiMessage midiMachineControlGoto(int hours, int minutes, int seconds, int frames)
static MidiMessage midiMachineControlCommand(MidiMachineControlCommand command)
static bool JUCE_CALLTYPE isForegroundProcess()
static Result fail(const String &errorMessage) noexcept
static Time JUCE_CALLTYPE getCurrentTime() noexcept
static uint32 getMillisecondCounter() noexcept
void startTimerHz(int timerFrequencyHz) noexcept
bool hasType(const Identifier &typeName) const noexcept
The Tracktion Edit class!
void restartPlayback()
Use this to tell the play engine to rebuild the audio graph if the toplogy has changed.
TimeDuration getTimecodeOffset() const noexcept
Returns the offset to apply to MIDI timecode.
TransportControl & getTransport() const noexcept
Returns the TransportControl which is used to stop/stop/position playback and recording.
void sendStartStopMessageToPlugins()
Calls Plugin::playStartedOrStopped to handle automation reacording.
TimecodeDisplayFormat getTimecodeFormat() const
Returns the current TimecodeDisplayFormat.
void setClickTrackRange(TimeRange) noexcept
Sets a range for the click track to be audible within.
TimeDuration getLength() const
Returns the end time of last clip.
bool isTimecodeSyncEnabled() const noexcept
Returns true if syncing to MIDI timecode is enabled.
juce::CachedValue< bool > playInStopEnabled
Whether the audio engine should run when playback is stopped.
bool shouldPlay() const noexcept
Returns true if this Edit should be played back (or false if it was just opened for inspection).
AbletonLink & getAbletonLink() const noexcept
Returns the AbletonLink object.
AutomationRecordManager & getAutomationRecordManager() noexcept
Returns the AutomationRecordManager for the Edit.
bool isRendering() const noexcept
Returns true if the Edit is currently being rendered.
TempoSequence tempoSequence
The global TempoSequence of this Edit.
TimePosition getPreviousTimeOfInterest(TimePosition beforeThisTime)
Finds the previous marker or start/end of a clip after a certain time.
int getNumCountInBeats() const
Returns the number of beats of the count in.
void updateMidiTimecodeDevices()
Updates the MIDI timecode/MMC devices.
juce::CachedValue< bool > recordingPunchInOut
Whether recoridng only happens within the in/out markers.
static TimeDuration getMaximumLength()
Returns the maximum length an Edit can be.
TimePosition getNextTimeOfInterest(TimePosition afterThisTime)
Finds the next marker or start/end of a clip after a certain time.
The Engine is the central class for all tracktion sessions.
PropertyStorage & getPropertyStorage() const
Returns the PropertyStorage user settings customisable XML file.
UIBehaviour & getUIBehaviour() const
Returns the UIBehaviour class.
ExternalControllerManager & getExternalControllerManager() const
Returns the ExternalControllerManager instance.
DeviceManager & getDeviceManager() const
Returns the DeviceManager instance for handling audio / MIDI devices.
ActiveEdits & getActiveEdits() const noexcept
Returns the ActiveEdits instance.
An instance of an InputDevice that's available to an Edit.
static juce::File renderToFile(const juce::String &taskDescription, const Parameters &params)
Renders an Edit to a file given by the Parameters.
TimePosition toTime(BeatPosition) const
Converts a number of beats a time.
BeatPosition toBeats(TimePosition) const
Converts a time to a number of beats.
TimeSigSetting * getTimeSig(int index) const
Returns the TimeSigSetting at a given index.
TempoSetting & getTempoAt(TimePosition) const
Returns the TempoSetting at the given position.
Controls the transport of an Edit's playback.
void playSectionAndReset(TimeRange rangeToPlay)
Plays a section of an Edit then stops playback, useful for previewing clips.
void setLoopIn(TimePosition)
Sets the loop in position.
void setFastForwardButtonDown(bool isDown)
Starts/stops a fast-forward operation.
TimePosition getPosition() const
Returns the current transport position.
void setLoopPoint2(TimePosition)
Sets a loop point 2 position.
void ensureContextAllocated(bool alwaysReallocate=false)
Ensures an active EditPlaybackContext has been created so this Edit can be played back.
bool isPlayContextActive() const
Returns true if this Edit is attached to the DeviceManager for playback.
void syncToEdit(Edit *editToSyncTo, bool syncToTargetLoopLength)
Syncs this Edit's playback to another Edit.
juce::ValueTree state
The state of this transport.
void setLoopRange(TimeRange)
Sets the loop points from a given range.
juce::Result applyRetrospectiveRecord(bool armedOnly)
Applys a retrospective record to any assigned input devices, creating clips for any historical input.
juce::Array< juce::File > getRetrospectiveRecordAsAudioFiles()
Perfoms a retrospective record operation and returns any new files.
void nudgeRight()
Moves the transport forwards slightly.
EditPlaybackContext * getCurrentPlaybackContext() const
Returns the active EditPlaybackContext if this Edit is attached to the DeviceManager for playback.
void freePlaybackContext()
Detaches the current EditPlaybackContext, removing it from the DeviceManager.
bool isRecordingStopping() const
Returns true if a recording is currently being stopped.
bool isRecording() const
Returns true if recording is in progress.
void setLoopPoint1(TimePosition)
Sets a loop point 1 position.
void play(bool justSendMMCIfEnabled)
Starts playback of an Edit.
void stopRecording(bool discardRecordings=false)
Stops recording without stopping playback.
bool isUserDragging() const noexcept
Returns true if a drag/scrub operation has been enabled.
void setUserDragging(bool)
Signifies a scrub-drag operation has started/stopped.
void forceOrphanFreezeAndProxyFilesPurge()
Triggers a cleanup of any unused freeze and proxy files.
void playFromStart(bool justSendMMCIfEnabled)
Sets the position to the startPosition and begins playback from there.
void nudgeLeft()
Moves the transport back slightly.
static juce::Array< TransportControl * > getAllActiveTransports(Engine &)
Returns all the active TransportControl[s] in the Engine.
TimePosition getTimeWhenStarted() const
Returns the time when the transport was started.
bool isStopping() const
Returns true if the transport is currently being stopped.
bool isPositionUpdatingFromPlayhead() const
Returns true if the current position change was triggered from an update directly from the playhead (...
void editHasChanged()
Triggers a playback graph rebuild.
void stop(bool discardRecordings, bool clearDevices, bool canSendMMCStop=true)
Stops recording, creating clips of newyly recorded files/MIDI data.
void record(bool justSendMMCIfEnabled, bool allowRecordingIfNoInputsArmed=false)
Starts recording.
TimeRange getLoopRange() const noexcept
Returns the loop range.
void setPosition(TimePosition)
Sets a new transport position.
void setLoopOut(TimePosition)
Sets a loop out position.
bool isSafeRecording() const
Returns true if safe-recording is in progress.
TransportControl(Edit &, const juce::ValueTree &)
Constructs a TransportControl for an Edit.
void setRewindButtonDown(bool isDown)
Starts/stops a rewind operation.
bool isPlaying() const
Returns true if the transport is playing.
Edit & edit
The Edit this transport belongs to.
void setSnapType(TimecodeSnapType)
Sets a snap type to use.
void stopIfRecording()
Stops playback only if recording is currently in progress.
Engine & engine
The Engine this Edit belongs to.
juce::CachedValue< TimePosition > startPosition
The position to start playing from.
void triggerClearDevicesOnStop()
Triggers a graph rebuild when playback stops.
virtual void showWarningMessage(const juce::String &message)
Should display a temporary warning message.
An audio clip that uses an audio file as its source.
Converts a monotonically increasing reference range in to a timeline range.
T fmod(T... args)
T is_pointer_v
#define TRANS(stringLiteral)
#define jassert(expression)
#define JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(className)
typedef int
T make_pair(T... args)
T max(T... args)
T min(T... args)
constexpr bool approximatelyEqual(Type a, Type b, Tolerance< Type > tolerance=Tolerance< Type >{} .withAbsolute(std::numeric_limits< Type >::min()) .withRelative(std::numeric_limits< Type >::epsilon()))
Type jlimit(Type lowerLimit, Type upperLimit, Type valueToConstrain) noexcept
TimeRange getTimeRangeForSelectedItems(const SelectableList &selected)
Returns the time range covered by the given items.
juce::Array< Track * > getAllTracks(const Edit &edit)
Returns all the tracks in an Edit.
void markOut(TransportControl &tc)
Sets the mark out position to the current transport position.
void toStart(TransportControl &tc, const SelectableList &items)
Moves the transport to the start of the selected objects.
void freePlaybackContextIfNotRecording(TransportControl &tc)
Frees the playback context if no recording is in progress, useful for when an app is minimised.
void toEnd(TransportControl &tc, const SelectableList &items)
Moves the transport to the end of the selected objects.
bool isRecording(EditPlaybackContext &epc)
Returns true if any inputs are currently recording.
void markIn(TransportControl &tc)
Sets the mark in position to the current transport position.
void tabBack(TransportControl &tc)
Moves the transport back to the previous point of interest.
void tabForward(TransportControl &tc)
Moves the transport forwards to the next point of interest.
void scrub(TransportControl &tc, double units)
Scrubs back and forth in 'units', where a unit is about 1/50th of the width of the strip window.
TimeRange timeRangeFromSamples(juce::Range< int64_t > sampleRange, double sampleRate)
Creates a TimeRange from a range of samples.
constexpr int64_t toSamples(TimePosition, double sampleRate)
Converts a TimePosition to a number of samples.
RangeType< TimePosition > TimeRange
A RangeType based on real time (i.e.
constexpr TimeDuration toDuration(TimePosition)
Converts a TimePosition to a TimeDuration.
T push_back(T... args)
T reset(T... args)
Represents a position in real-life time.
constexpr double inSeconds() const
Returns the TimePosition as a number of seconds.
ID for objects of type EditElement - e.g.
A list of Selectables, similar to a juce::Array but contains a cached list of the SelectableClasses f...
virtual void autoSaveNow()
Called periodically to indicate the Edit has changed in an audible way and should be auto-saved.
virtual void stopVideo()
Should stop video playback.
virtual void recordingAboutToStart(InputDeviceInstance &, EditItemID)
Called before recording start for a specific input instance.
virtual void recordingFinished(InputDeviceInstance &, EditItemID, const juce::ReferenceCountedArray< Clip > &)
Called when recording stops for a specific input instance.
virtual void recordingAboutToStop(InputDeviceInstance &, EditItemID)
Called before recording stops for a specific input instance.
virtual void recordingStopped(SyncPoint, bool)
Called when global recording stops.
virtual void setAllLevelMetersActive(bool)
If false, levels should be cleared.
virtual void startVideo()
Should start video playback.
TimePosition getLiveTransportPosition() const
Returns the transport position to show in the UI, taking in to account any latency.
ReallocationInhibitor(TransportControl &)
Stops an Edit creating a new playback graph.
void stop(bool discardRecordings_, bool clearDevices_, bool canSendMMCStop_)
Stop playback/recording.
void play(bool justSendMMCIfEnabled_)
Start playback from the current transport position.
void record(bool justSendMMCIfEnabled_, bool allowRecordingIfNoInputsArmed_)
Start recording.
void setVideoPosition(TimePosition time, bool forceJump)
Updates the current video position, calling any listeners.
time
times
#define CRASH_TRACER
This macro adds the current location to a stack which gets logged if a crash happens.