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

« « « Anklang Documentation
Loading...
Searching...
No Matches
tracktion_TempoSequence.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
14template<typename ObjectType>
15struct TempoAndTimeSigListBase : public ValueTreeObjectList<ObjectType>,
17{
19 : ValueTreeObjectList<ObjectType> (parentTree), sequence (ts)
20 {
21 }
22
23 void newObjectAdded (ObjectType*) override { sendChange(); }
24 void objectRemoved (ObjectType*) override { sendChange(); }
25 void objectOrderChanged() override { sendChange(); }
26 void valueTreePropertyChanged (juce::ValueTree&, const juce::Identifier&) override { sendChange(); }
27
28 void sendChange()
29 {
30 if (! sequence.getEdit().isLoading())
32 }
33
34 void handleAsyncUpdate() override
35 {
36 sequence.updateTempoData();
37 }
38
39 TempoSequence& sequence;
40};
41
42//==============================================================================
44{
45 TempoSettingList (TempoSequence& ts, const juce::ValueTree& parentTree)
47 {
48 rebuildObjects();
49 }
50
51 ~TempoSettingList() override
52 {
53 freeObjects();
54 }
55
56 bool isSuitableType (const juce::ValueTree& v) const override
57 {
58 return v.hasType (IDs::TEMPO);
59 }
60
61 TempoSetting* createNewObject (const juce::ValueTree& v) override
62 {
63 auto t = new TempoSetting (sequence, v);
64 t->incReferenceCount();
65 return t;
66 }
67
68 void deleteObject (TempoSetting* t) override
69 {
70 jassert (t != nullptr);
71 t->decReferenceCount();
72 }
73};
74
75//==============================================================================
77{
78 TimeSigList (TempoSequence& ts, const juce::ValueTree& parentTree)
80 {
81 rebuildObjects();
82 }
83
84 ~TimeSigList() override
85 {
86 freeObjects();
87 }
88
89 bool isSuitableType (const juce::ValueTree& v) const override
90 {
91 return v.hasType (IDs::TIMESIG);
92 }
93
94 TimeSigSetting* createNewObject (const juce::ValueTree& v) override
95 {
96 auto t = new TimeSigSetting (sequence, v);
97 t->incReferenceCount();
98 return t;
99 }
100
101 void deleteObject (TimeSigSetting* t) override
102 {
103 jassert (t != nullptr);
104 t->decReferenceCount();
105 }
106};
107
108
109//==============================================================================
111{
112}
113
115{
117 notifyListenersOfDeletion();
118}
119
120juce::UndoManager* TempoSequence::getUndoManager() const noexcept
121{
122 return &edit.getUndoManager();
123}
124
125//==============================================================================
126void TempoSequence::setState (const juce::ValueTree& v, bool remapEdit)
127{
129
130 if (remapEdit)
131 snap.savePreChangeState (edit);
132
133 freeResources();
134
135 state = v;
136
137 tempos = std::make_unique<TempoSettingList> (*this, state);
138 timeSigs = std::make_unique<TimeSigList> (*this, state);
139
140 if (tempos->objects.isEmpty())
141 insertTempo ({}, nullptr);
142
143 if (timeSigs->objects.isEmpty())
144 insertTimeSig ({}, nullptr);
145
146 tempos->objects.getFirst()->startBeatNumber = BeatPosition();
147 timeSigs->objects.getFirst()->startBeatNumber = BeatPosition();
148
149 for (int i = 1; i < getNumTempos(); ++i)
150 getTempo (i)->startBeatNumber = std::max (getTempo (i)->startBeatNumber.get(),
151 getTempo (i - 1)->startBeatNumber.get());
152
153 for (int i = 1; i < getNumTimeSigs(); ++i)
154 getTimeSig (i)->startBeatNumber = std::max (getTimeSig (i)->startBeatNumber.get(),
155 getTimeSig (i - 1)->startBeatNumber.get() + BeatDuration::fromBeats (1));
156
157 updateTempoData();
158
159 if (remapEdit)
160 snap.remapEdit (edit);
161}
162
164{
165 setState (juce::ValueTree (IDs::TEMPOSEQUENCE), false);
166}
167
169{
170 copyValueTree (state, other.state, nullptr);
171}
172
173void TempoSequence::freeResources()
174{
175 tempos.reset();
176 timeSigs.reset();
177}
178
179const juce::Array<TimeSigSetting*>& TempoSequence::getTimeSigs() const { return timeSigs->objects; }
180int TempoSequence::getNumTimeSigs() const { return timeSigs->objects.size(); }
181TimeSigSetting* TempoSequence::getTimeSig (int index) const { return timeSigs->objects[index]; }
182
183const juce::Array<TempoSetting*>& TempoSequence::getTempos() const { return tempos->objects; }
184int TempoSequence::getNumTempos() const { return tempos->objects.size(); }
185TempoSetting* TempoSequence::getTempo (int index) const { return tempos->objects[index]; }
186
188{
189 return insertTempo (time, getUndoManager());
190}
191
193{
194 return insertTempo (beatNum, bpm, curve, getUndoManager());
195}
196
198{
199 auto bpm = getBpmAt (time);
200 float defaultCurve = 1.0f;
201
202 if (getNumTempos() > 0)
203 return insertTempo (tracktion::roundToNearestBeat (toBeats (time)), bpm, defaultCurve, um);
204
205 return insertTempo ({}, bpm, defaultCurve, um);
206}
207
208TempoSetting::Ptr TempoSequence::insertTempo (BeatPosition beatNum, double bpm, float curve, juce::UndoManager* um)
209{
210 int index = -1;
211
212 if (getNumTempos() > 0)
213 index = state.indexOf (getTempoAt (beatNum).state) + 1;
214
215 state.addChild (TempoSetting::create (beatNum, bpm, curve), index, um);
216
217 return getTempoAt (beatNum);
218}
219
221{
222 return insertTimeSig (time, getUndoManager());
223}
224
229
231{
232 BeatPosition beatNum;
233 int index = -1;
234
235 auto newTree = createValueTree (IDs::TIMESIG,
236 IDs::numerator, 4,
237 IDs::denominator, 4);
238
239 if (getNumTimeSigs() > 0)
240 {
241 beatNum = tracktion::roundToNearestBeat (toBeats (time));
242 auto& prev = getTimeSigAt (beatNum);
243
244 index = state.indexOf (prev.state) + 1;
245 newTree = prev.state.createCopy();
246
247 // don't add in the same place as another one
248 if (prev.startBeatNumber == beatNum)
249 return prev;
250 }
251
252 newTree.setProperty (IDs::startBeat, beatNum.inBeats(), nullptr);
253
254 state.addChild (newTree, index, um);
255
256 return getTimeSigAt (beatNum);
257}
258
259void TempoSequence::removeTempo (int index, bool remapEdit)
260{
261 if (index == 0)
262 return;
263
264 if (auto ts = getTempo (index))
265 {
266 if (getNumTempos() > 1)
267 {
269
270 if (remapEdit)
271 snap.savePreChangeState (edit);
272
273 jassert (ts->state.isAChildOf (state));
274 state.removeChild (ts->state, getUndoManager());
275
276 if (remapEdit)
277 snap.remapEdit (edit);
278 }
279 }
280}
281
282void TempoSequence::removeTemposBetween (TimeRange range, bool remapEdit)
283{
285
286 if (remapEdit)
287 snap.savePreChangeState (edit);
288
289 for (int i = getNumTempos(); --i > 0;)
290 if (auto ts = getTempo (i))
291 if (range.contains (ts->getStartTime()))
292 removeTempo (i, false);
293
294 if (remapEdit)
295 snap.remapEdit (edit);
296}
297
299{
300 if (index > 0)
301 {
302 if (auto ts = getTimeSig (index))
303 {
304 jassert (ts->state.isAChildOf (state));
305 state.removeChild (ts->state, getUndoManager());
306 }
307 }
308}
309
311{
312 for (int i = getNumTimeSigs(); --i > 0;)
313 if (auto ts = getTimeSig (i))
314 if (range.contains (ts->getPosition().getStart()))
315 removeTimeSig (i);
316}
317
318void TempoSequence::moveTempoStart (int index, BeatDuration deltaBeats, bool snapToBeat)
319{
320 if (index > 0 && deltaBeats != BeatDuration())
321 {
322 if (auto t = getTempo (index))
323 {
324 auto prev = getTempo (index - 1);
325 auto next = getTempo (index + 1);
326
327 const auto prevBeat = prev != nullptr ? prev->startBeatNumber : BeatPosition();
328 const auto nextBeat = next != nullptr ? next->startBeatNumber : BeatPosition::fromBeats (0x7ffffff);
329
330 const auto newStart = juce::jlimit (prevBeat, nextBeat, t->startBeatNumber.get() + deltaBeats);
331 t->set (snapToBeat ? tracktion::roundToNearestBeat (newStart) : newStart,
332 t->bpm, t->curve, false);
333 }
334 }
335}
336
337void TempoSequence::moveTimeSigStart (int index, BeatDuration deltaBeats, bool snapToBeat)
338{
339 if (index > 0 && deltaBeats != BeatDuration())
340 {
341 if (auto t = getTimeSig (index))
342 {
343 auto prev = getTimeSig (index - 1);
344 auto next = getTimeSig (index + 1);
345
346 const auto prevBeat = prev != nullptr ? prev->startBeatNumber : BeatPosition();
347 const auto nextBeat = next != nullptr ? next->startBeatNumber : BeatPosition::fromBeats (0x7ffffff);
348
349 if (nextBeat < prevBeat + BeatDuration::fromBeats (2))
350 return;
351
352 t->startBeatNumber.forceUpdateOfCachedValue();
353 const auto newBeat = t->startBeatNumber.get() + deltaBeats;
354 t->startBeatNumber = juce::jlimit (prevBeat + BeatDuration::fromBeats (1), nextBeat - BeatDuration::fromBeats (1),
355 snapToBeat ? tracktion::roundToNearestBeat (newBeat) : newBeat);
356 }
357 }
358}
359
360void TempoSequence::insertSpaceIntoSequence (TimePosition time, TimeDuration amountOfSpaceInSeconds, bool snapToBeat)
361{
362 // there may be a temp change at this time so we need to find the tempo to the left of it hence the nudge
363 time = time - TimeDuration::fromSeconds (0.00001);
364 const auto beatsToInsert = BeatDuration::fromBeats (getBeatsPerSecondAt (time) * amountOfSpaceInSeconds.inSeconds());
365
366 // Move timesig settings
367 {
368 const int endIndex = indexOfTimeSigAt (time) + 1;
369
370 for (int i = getNumTimeSigs(); --i >= endIndex;)
371 moveTimeSigStart (i, beatsToInsert, snapToBeat);
372 }
373
374 // Move tempo settings
375 {
376 const int endIndex = indexOfNextTempoAt (time);
377
378 for (int i = getNumTempos(); --i >= endIndex;)
379 moveTempoStart (i, beatsToInsert, snapToBeat);
380 }
381}
382
383void TempoSequence::deleteRegion (TimeRange range)
384{
385 const auto beatRange = toBeats (range);
386
387 removeTemposBetween (range, false);
388 removeTimeSigsBetween (range);
389
390 const bool snapToBeat = false;
391 const auto startTime = toTime (beatRange.getStart());
392 const auto deltaBeats = -beatRange.getLength();
393
394 // Move timesig settings
395 {
396 const int startIndex = indexOfTimeSigAt (startTime) + 1;
397
398 for (int i = startIndex; i < getNumTimeSigs(); ++i)
399 moveTimeSigStart (i, deltaBeats, snapToBeat);
400 }
401
402 // Move tempo settings
403 {
404 const int startIndex = indexOfNextTempoAt (startTime);
405
406 for (int i = startIndex; i < getNumTempos(); ++i)
407 moveTempoStart (i, deltaBeats, snapToBeat);
408 }
409}
410
411//==============================================================================
416
418{
419 for (int i = getNumTimeSigs(); --i >= 0;)
420 if (timeSigs->objects.getUnchecked (i)->startBeatNumber <= beat)
421 return *timeSigs->objects.getUnchecked (i);
422
423 jassert (timeSigs->size() > 0);
424 return *timeSigs->objects.getFirst();
425}
426
428{
429 for (int i = getNumTimeSigs(); --i >= 0;)
430 if (timeSigs->objects.getUnchecked (i)->startTime <= t)
431 return i;
432
433 jassert (timeSigs->size() > 0);
434 return 0;
435}
436
437int TempoSequence::indexOfTimeSig (const TimeSigSetting* timeSigSetting) const
438{
439 return timeSigs->objects.indexOf (const_cast<TimeSigSetting*> (timeSigSetting));
440}
441
442//==============================================================================
447
449{
450 for (int i = getNumTempos(); --i >= 0;)
451 if (tempos->objects.getUnchecked (i)->startBeatNumber <= beat)
452 return *tempos->objects.getUnchecked (i);
453
454 jassert (tempos->size() > 0);
455 return *tempos->objects.getFirst();
456}
457
459{
460 for (int i = getNumTempos(); --i >= 0;)
461 if (tempos->objects.getUnchecked (i)->getStartTime() <= t)
462 return i;
463
464 jassert (tempos->size() > 0);
465 return 0;
466}
467
469{
470 for (int i = 0; i < getNumTempos(); ++i)
471 if (tempos->objects.getUnchecked (i)->getStartTime() >= t)
472 return i;
473
474 return getNumTempos();
475}
476
478{
479 return tempos->objects.indexOf (const_cast<TempoSetting*> (t));
480}
481
483{
484 updateTempoDataIfNeeded();
485 return internalSequence.getBpmAt (time);
486}
487
488double TempoSequence::getBeatsPerSecondAt (TimePosition time, bool lengthOfOneBeatDependsOnTimeSignature) const
489{
490 if (lengthOfOneBeatDependsOnTimeSignature)
491 {
492 updateTempoDataIfNeeded();
493 return internalSequence.getBeatsPerSecondAt (time).v;
494 }
495
496 return getBpmAt (time) / 60.0;
497}
498
500{
501 return getTimeSigAt (time).triplets;
502}
503
504//==============================================================================
506{
507 updateTempoDataIfNeeded();
508 return internalSequence.toBeats (time);
509}
510
511BeatRange TempoSequence::toBeats (TimeRange range) const
512{
513 return { toBeats (range.getStart()),
514 toBeats (range.getEnd()) };
515}
516
517BeatPosition TempoSequence::toBeats (tempo::BarsAndBeats barsBeats) const
518{
519 return toBeats (toTime (barsBeats));
520}
521
523{
524 updateTempoDataIfNeeded();
525 return internalSequence.toTime (beats);
526}
527
528TimeRange TempoSequence::toTime (BeatRange range) const
529{
530 return { toTime (range.getStart()),
531 toTime (range.getEnd()) };
532}
533
534TimePosition TempoSequence::toTime (tempo::BarsAndBeats barsBeats) const
535{
536 updateTempoDataIfNeeded();
537 return internalSequence.toTime (barsBeats);
538}
539
540tempo::BarsAndBeats TempoSequence::toBarsAndBeats (TimePosition t) const
541{
542 updateTempoDataIfNeeded();
543 return internalSequence.toBarsAndBeats (t);
544}
545
546//==============================================================================
547const tempo::Sequence& TempoSequence::getInternalSequence() const
548{
549 return internalSequence;
550}
551
552void TempoSequence::updateTempoData()
553{
554 tempos->cancelPendingUpdate();
555 jassert (getNumTempos() > 0 && getNumTimeSigs() > 0);
556
557 // Build the new sequence
561
562 // Copy change events
563 {
564 for (auto ts : getTempos())
565 tempoChanges.push_back ({ ts->startBeatNumber.get(), ts->bpm.get(), ts->curve.get() });
566
567 for (auto ts : getTimeSigs())
568 timeSigChanges.push_back ({ ts->startBeatNumber.get(), ts->numerator.get(), ts->denominator.get(), ts->triplets.get() });
569
570 for (auto pc : edit.pitchSequence.getPitches())
571 keyChanges.push_back ({ pc->startBeat.get(), { pc->pitch.get(), static_cast<int> (pc->scale.get()) } });
572 }
573
575 tempo::Sequence newSeq (std::move (tempoChanges), std::move (timeSigChanges), std::move (keyChanges),
576 useDenominator ? tempo::LengthOfOneBeat::dependsOnTimeSignature
578
579 // Update startTime properties of the model objects
580 {
581 for (auto ts : getTempos())
582 ts->startTime = newSeq.toTime (ts->startBeatNumber.get());
583
584 for (auto ts : getTimeSigs())
585 ts->startTime = newSeq.toTime (ts->startBeatNumber.get());
586 }
587
588 jassert (getNumTempos() > 0 && getNumTimeSigs() > 0);
590
591 {
592 //TODO: This lock should be removed when all playback classes are using the new tempo::Sequence class
594 internalSequence = std::move (newSeq);
595 }
596}
597
598void TempoSequence::updateTempoDataIfNeeded() const
599{
600 // The check on isUpdatePending is to avoid triggering a message thread assert
601 // if this is called on a background thread when no update is needed
602 if (tempos->isUpdatePending())
603 tempos->handleUpdateNowIfNeeded();
604
605 if (timeSigs->isUpdatePending())
606 timeSigs->handleUpdateNowIfNeeded();
607}
608
609void TempoSequence::handleAsyncUpdate()
610{
612 changed();
613}
614
616{
617 return TRANS("Tempo Curve");
618}
619
620int TempoSequence::countTemposInRegion (TimeRange range) const
621{
622 int count = 0;
623
624 for (auto t : tempos->objects)
625 if (range.contains (t->getStartTime()))
626 ++count;
627
628 return count;
629}
630
631HashCode TempoSequence::createHashForTemposInRange (TimeRange range) const
632{
633 HashCode hash = 0;
634
635 for (auto t : tempos->objects)
636 if (range.contains (t->getStartTime()))
637 hash ^= t->getHash();
638
639 return hash;
640}
641
642//==============================================================================
643tempo::BarsAndBeats TempoSequence::timeToBarsBeats (TimePosition t) const
644{
645 return toBarsAndBeats (t);
646}
647
648TimePosition TempoSequence::barsBeatsToTime (tempo::BarsAndBeats barsBeats) const
649{
650 return toTime (barsBeats);
651}
652
653BeatPosition TempoSequence::barsBeatsToBeats (tempo::BarsAndBeats barsBeats) const
654{
655 return toBeats (barsBeats);
656}
657
658BeatPosition TempoSequence::timeToBeats (TimePosition time) const
659{
660 return toBeats (time);
661}
662
663BeatRange TempoSequence::timeToBeats (TimeRange range) const
664{
665 return toBeats (range);
666}
667
668TimePosition TempoSequence::beatsToTime (BeatPosition beats) const
669{
670 return toTime (beats);
671}
672
673TimeRange TempoSequence::beatsToTime (BeatRange range) const
674{
675 return toTime (range);
676}
677
678TimeSigSetting& TempoSequence::getTimeSigAtBeat (BeatPosition beat) const
679{
680 return getTimeSigAt (beat);
681}
682
683TempoSetting& TempoSequence::getTempoAtBeat (BeatPosition beat) const
684{
685 return getTempoAt (beat);
686}
687
688//==============================================================================
689//==============================================================================
694
695
696//==============================================================================
697//==============================================================================
698void EditTimecodeRemapperSnapshot::savePreChangeState (Edit& ed)
699{
700 auto& tempoSequence = ed.tempoSequence;
701
702 clips.clear();
703
704 auto addClip = [this, &tempoSequence] (auto clip)
705 {
706 auto pos = clip->getPosition();
707
708 ClipPos cp;
709 cp.clip = clip;
710 cp.startBeat = tempoSequence.toBeats (pos.getStart());
711 cp.endBeat = tempoSequence.toBeats (pos.getEnd());
712 cp.contentStartBeat = toDuration (tempoSequence.toBeats (pos.getStartOfSource()));
713 clips.add (cp);
714 };
715
716 for (auto t : getClipTracks (ed))
717 {
718 for (auto& c : t->getClips())
719 {
720 addClip (c);
721
722 if (auto cc = dynamic_cast<ClipOwner*> (c))
723 for (auto childClip : cc->getClips())
724 addClip (childClip);
725 }
726
727 if (auto at = dynamic_cast<AudioTrack*> (t))
728 for (auto slot : at->getClipSlotList().getClipSlots())
729 if (auto cc = slot->getClip())
730 addClip (cc);
731 }
732
733 automation.clear();
734
735 for (auto aei : getAllAutomatableEditItems (ed))
736 {
737 if (aei == nullptr)
738 continue;
739
740 for (auto ap : aei->getAutomatableParameters())
741 {
742 if (! ap->automatableEditElement.remapOnTempoChange)
743 continue;
744
745 auto& curve = ap->getCurve();
746 auto numPoints = curve.getNumPoints();
747
748 if (numPoints == 0)
749 continue;
750
752 beats.ensureStorageAllocated (numPoints);
753
754 for (int p = 0; p < numPoints; ++p)
755 beats.add (toBeats (curve.getPoint (p).time, tempoSequence));
756
757 automation.add ({ curve, std::move (beats) });
758 }
759 }
760
761 const auto loopRange = ed.getTransport().getLoopRange();
762 loopPositionBeats = { tempoSequence.toBeats (loopRange.getStart()),
763 tempoSequence.toBeats (loopRange.getEnd()) };
764
765 startPositionBeats = tempoSequence.toBeats (ed.getTransport().startPosition);
766}
767
768void EditTimecodeRemapperSnapshot::remapEdit (Edit& ed)
769{
770 auto& transport = ed.getTransport();
771 auto& tempoSequence = ed.tempoSequence;
772 tempoSequence.updateTempoData();
773
774 transport.startPosition = tempoSequence.toTime (startPositionBeats);
775 transport.setLoopRange (tempoSequence.toTime (loopPositionBeats));
776
777 for (auto& cp : clips)
778 {
779 if (auto c = cp.clip.get())
780 {
781 auto newStart = tempoSequence.toTime (cp.startBeat);
782 auto newEnd = tempoSequence.toTime (cp.endBeat);
783 auto newOffset = newStart - tempoSequence.toTime (toPosition (cp.contentStartBeat));
784
785 auto pos = c->getPosition();
786
787 if (std::abs ((pos.getStart() - newStart).inSeconds())
788 + std::abs ((pos.getEnd() - newEnd).inSeconds())
789 + std::abs ((pos.getOffset() - newOffset).inSeconds()) > 0.001)
790 {
791 if (c->getSyncType() == Clip::syncAbsolute)
792 continue;
793
794 if (c->type == TrackItem::Type::wave)
795 {
796 auto ac = dynamic_cast<AudioClipBase*> (c);
797
798 if (ac != nullptr && ac->getAutoTempo())
799 c->setPosition ({ { newStart, newEnd }, newOffset });
800 else
801 c->setStart (newStart, false, true);
802 }
803 else
804 {
805 c->setPosition ({ { newStart, newEnd }, newOffset });
806 }
807 }
808 }
809 }
810
811 const ParameterChangeHandler::Disabler disabler (ed.getParameterChangeHandler());
812
813 for (auto& a : automation)
814 for (int i = a.beats.size(); --i >= 0;)
815 a.curve.setPointTime (i, tempoSequence.toTime (a.beats.getUnchecked (i)));
816}
817
818#if TRACKTION_UNIT_TESTS && ENGINE_UNIT_TESTS_TEMPO_SEQUENCE
819
820//==============================================================================
821//==============================================================================
822class TempoSequenceTests : public juce::UnitTest
823{
824public:
825 TempoSequenceTests() : juce::UnitTest ("TempoSequence", "Tracktion") {}
826
827 //==============================================================================
828 void runTest() override
829 {
830 runPositionTests();
831 runModificationTests();
832 }
833
834private:
835 void expectBarsAndBeats (tempo::Sequence::Position& pos, int bars, int beats)
836 {
837 auto barsBeats = pos.getBarsBeats();
838 expectEquals (barsBeats.bars, bars);
839 expectEquals (barsBeats.getWholeBeats(), beats);
840 }
841
842 void expectTempoSetting (TempoSetting& tempo, double startTime, double bpm, float curve)
843 {
844 expectWithinAbsoluteError (tempo.getStartTime().inSeconds(), startTime, 0.001);
845 expectWithinAbsoluteError (tempo.getBpm(), bpm, 0.001);
846 expectWithinAbsoluteError (tempo.getCurve(), curve, 0.001f);
847 }
848
849 void runPositionTests()
850 {
851 auto edit = Edit::createSingleTrackEdit (*Engine::getEngines().getFirst());
852
853 beginTest ("Defaults");
854 {
855 auto pos = createPosition (edit->tempoSequence);
856
857 expectEquals (pos.getTempo(), 120.0);
858 expectEquals (pos.getTimeSignature().numerator, 4);
859 expectEquals (pos.getTimeSignature().denominator, 4);
860 expectEquals (pos.getPPQTimeOfBarStart(), 0.0);
861 expectEquals (pos.getBarsBeats().numerator, 4);
862 }
863
864 beginTest ("Positive sequences");
865 {
866 auto pos = createPosition (edit->tempoSequence);
867 pos.set (0_tp);
868
869 expectBarsAndBeats (pos, 0, 0);
870 pos.add (1_bd);
871 expectBarsAndBeats (pos, 0, 1);
872 pos.add (1_bd);
873 expectBarsAndBeats (pos, 0, 2);
874 pos.add (1_bd);
875 expectBarsAndBeats (pos, 0, 3);
876 pos.add (1_bd);
877 expectBarsAndBeats (pos, 1, 0);
878
879 pos.addBars (1);
880 expectBarsAndBeats (pos, 2, 0);
881 pos.add (1_bd);
882 expectBarsAndBeats (pos, 2, 1);
883 pos.add (1_bd);
884 expectBarsAndBeats (pos, 2, 2);
885 pos.add (1_bd);
886 expectBarsAndBeats (pos, 2, 3);
887 }
888
889 beginTest ("Negative sequences");
890 {
891 auto pos = createPosition (edit->tempoSequence);
892 pos.set (0_tp);
893 pos.addBars (-2);
894
895 expectBarsAndBeats (pos, -2, 0);
896 pos.add (1_bd);
897 expectBarsAndBeats (pos, -2, 1);
898 pos.add (1_bd);
899 expectBarsAndBeats (pos, -2, 2);
900 pos.add (1_bd);
901 expectBarsAndBeats (pos, -2, 3);
902 pos.add (1_bd);
903 expectBarsAndBeats (pos, -1, 0);
904 pos.add (1_bd);
905 expectBarsAndBeats (pos, -1, 1);
906 pos.add (1_bd);
907 expectBarsAndBeats (pos, -1, 2);
908 pos.add (1_bd);
909 expectBarsAndBeats (pos, -1, 3);
910 pos.add (1_bd);
911 expectBarsAndBeats (pos, 0, 0);
912 pos.add (1_bd);
913 expectBarsAndBeats (pos, 0, 1);
914 pos.add (1_bd);
915 expectBarsAndBeats (pos, 0, 2);
916 pos.add (1_bd);
917 expectBarsAndBeats (pos, 0, 3);
918 pos.add (1_bd);
919 expectBarsAndBeats (pos, 1, 0);
920 pos.add (1_bd);
921 }
922 }
923
924 void runModificationTests()
925 {
926 using namespace std::literals;
927 auto edit = Edit::createSingleTrackEdit (*Engine::getEngines()[0]);
928 auto& ts = edit->tempoSequence;
929
930 beginTest ("Insertions");
931 {
932 expectWithinAbsoluteError (ts.getTempoAt (0_tp).getBpm(), 120.0, 0.001);
933 expectWithinAbsoluteError (ts.getTempoAt (0_tp).getCurve(), 1.0f, 0.001f);
934
935 ts.insertTempo (4_bp, 120, 0.0);
936 ts.insertTempo (4_bp, 300, 0.0);
937 ts.insertTempo (8_bp, 300, 0.0);
938
939 expectTempoSetting (*ts.getTempo (0), 0.0, 120.0, 1.0f);
940 expectTempoSetting (*ts.getTempo (1), 2.0, 120.0, 0.0f);
941 expectTempoSetting (*ts.getTempo (2), 2.0, 300.0, 0.0f);
942 expectTempoSetting (*ts.getTempo (3), 2.8, 300.0, 0.0f);
943
944 expectTempoSetting (ts.getTempoAt (0_tp), 0.0, 120.0, 1.0f);
945 expectTempoSetting (ts.getTempoAt (2.0s), 2.0, 300.0, 0.0f);
946 expectTempoSetting (ts.getTempoAt (3.0s), 2.8, 300.0, 0.0f);
947 }
948 }
949};
950
951static TempoSequenceTests tempoSequenceTests;
952
953#endif
954
955}} // namespace tracktion { inline namespace engine
void ensureStorageAllocated(int minNumElements)
void add(const ElementType &newElement)
void clear()
void cancelPendingUpdate() noexcept
CriticalSection & getAudioCallbackLock() noexcept
void expectEquals(ValueType actual, ValueType expected, String failureMessage=String())
void beginTest(const String &testName)
void expectWithinAbsoluteError(ValueType actual, ValueType expected, ValueType maxAbsoluteError, String failureMessage=String())
virtual void runTest()=0
int indexOf(const ValueTree &child) const noexcept
@ syncAbsolute
Sync to abslute time.
The Tracktion Edit class!
void sendTempoOrPitchSequenceChangedUpdates()
Sends a message to all the clips to let them know the tempo or pitch sequence has changed.
bool isLoading() const
Returns true if the Edit's not yet fully loaded.
juce::UndoManager & getUndoManager() noexcept
Returns the juce::UndoManager used for this Edit.
Engine & engine
A reference to the Engine.
virtual bool lengthOfOneBeatDependsOnTimeSignature()
If this returns true, it means that the length (in seconds) of one "beat" at any point in an edit is ...
DeviceManager & getDeviceManager() const
Returns the DeviceManager instance for handling audio / MIDI devices.
EngineBehaviour & getEngineBehaviour() const
Returns the EngineBehaviour instance.
virtual void changed()
This should be called to send a change notification to any SelectableListeners that are registered wi...
Holds a list of TempoSetting objects, to form a sequence of tempo changes.
TimePosition toTime(BeatPosition) const
Converts a number of beats a time.
Edit & getEdit() const
Returns the Edit this TempoSequence refers to.
void moveTempoStart(int index, BeatDuration deltaBeats, bool snapToBeat)
Moves the TempoSetting at a given index by a number of beats.
void removeTimeSig(int index)
Removes the TimeSigSetting at a given index.
void insertSpaceIntoSequence(TimePosition time, TimeDuration amountOfSpaceInSeconds, bool snapToBeat)
Inserts space in to a sequence, shifting TempoSettings and TimeSigs.
void deleteRegion(TimeRange)
Removes a region in a sequence, shifting TempoSettings and TimeSigs.
void moveTimeSigStart(int index, BeatDuration deltaBeats, bool snapToBeat)
Moves the TimeSigSetting at a given index by a number of beats.
void createEmptyState()
Resets this to a default, empty state.
TimeSigSetting & getTimeSigAt(TimePosition) const
Returns the TimeSigSetting at a given position.
void removeTimeSigsBetween(TimeRange)
Removes any TimeSigSetting[s] within the range.
TempoSetting::Ptr insertTempo(TimePosition)
Inserts a tempo break that can be edited later.
const juce::Array< TimeSigSetting * > & getTimeSigs() const
Returns an array of the TimeSigSetting.
int countTemposInRegion(TimeRange) const
Returns the number of TempoSetting[s] in the given range.
int indexOfTempo(const TempoSetting *) const
Returns the index of the given TempoSetting.
int getNumTimeSigs() const
Returns the number of TimeSigSetting[s] in the sequence.
bool isTripletsAtTime(TimePosition) const
Returns true if the TempoSetting is triplets at the given time.
int indexOfTimeSig(const TimeSigSetting *) const
Returns the index of a given TimeSigSetting.
juce::String getSelectableDescription() override
Subclasses must return a description of what they are.
BeatPosition toBeats(TimePosition) const
Converts a time to a number of beats.
int getNumTempos() const
Returns the current number of TempoSettings.
void copyFrom(const TempoSequence &)
Copies the contents of another TempoSequence.
const juce::Array< TempoSetting * > & getTempos() const
Returns the TempoSettings.
tempo::BarsAndBeats toBarsAndBeats(TimePosition) const
Converts a time to a number of BarsAndBeats.
const tempo::Sequence & getInternalSequence() const
N.B.
void removeTemposBetween(TimeRange, bool remapEdit)
Removes any TempoSetting[s] within the range.
TimeSigSetting * getTimeSig(int index) const
Returns the TimeSigSetting at a given index.
void removeTempo(int index, bool remapEdit)
Removes the TempoSetting at a given index.
TempoSetting * getTempo(int index) const
Returns the TempoSetting at the given index.
int indexOfTimeSigAt(TimePosition) const
Returns the index of TimeSigSetting at a given position.
double getBeatsPerSecondAt(TimePosition, bool lengthOfOneBeatDependsOnTimeSignature=false) const
Returns the tempo at a given position.
TempoSetting & getTempoAt(TimePosition) const
Returns the TempoSetting at the given position.
void setState(const juce::ValueTree &, bool remapEdit)
Sets the state this TempoSequence should refer to.
Edit & edit
The Edit this sequence belongs to.
int indexOfNextTempoAt(TimePosition) const
Returns the index of the TempoSetting after the given position.
int indexOfTempoAt(TimePosition) const
Returns the index of the TempoSetting at the given position.
TempoSequence(Edit &)
Creates a TempoSequence for an Edit.
double getBpmAt(TimePosition) const
Returns the tempo at a given position.
TimeSigSetting::Ptr insertTimeSig(TimePosition)
Inserts a new TimeSigSetting at the given position.
A tempo value, as used in a TempoSequence.
static juce::ValueTree create(BeatPosition startBeat, double bpm, float curve)
Creates a tree to prepresent a TempoSetting.
T is_pointer_v
#define TRANS(stringLiteral)
#define jassert(expression)
typedef int
T max(T... args)
Type jlimit(Type lowerLimit, Type upperLimit, Type valueToConstrain) noexcept
tempo::Sequence::Position createPosition(const TempoSequence &s)
Creates a Position to iterate over the given TempoSequence.
juce::Array< ClipTrack * > getClipTracks(const Edit &edit)
Returns all the ClipTracks in an Edit.
BeatPosition toBeats(TimePosition tp, const TempoSequence &ts)
Converts a TimePosition to a BeatPosition given a TempoSequence.
juce::Array< AutomatableEditItem * > getAllAutomatableEditItems(const Edit &edit)
Returns all AutomatableEditItems in an Edit.
RangeType< BeatPosition > BeatRange
A RangeType based on beats.
RangeType< TimePosition > TimeRange
A RangeType based on real time (i.e.
T size(T... args)
Represents a duration in beats.
Represents a position in beats.
constexpr double inBeats() const
Returns the position as a number of beats.
Represents a duration in real-life time.
constexpr double inSeconds() const
Returns the TimeDuration as a number of seconds.
Represents a position in real-life time.
A Sequence::Position is an iterator through a Sequence.
Takes a copy of all the beat related things in an edit in terms of bars/beats and then remaps these a...
time
LengthOfOneBeat
Used to determine the length of a beat in beat <-> time conversions.
@ isAlwaysACrotchet
Signifies the length of one beat always depends only the current BPM at that point in the edit,...