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

« « « Anklang Documentation
Loading...
Searching...
No Matches
tracktion_QuantisationType.cpp
Go to the documentation of this file.
1 /*
2 ,--. ,--. ,--. ,--.
3 ,-' '-.,--.--.,--,--.,---.| |,-.,-' '-.`--' ,---. ,--,--, Copyright 2024
4 '-. .-'| .--' ,-. | .--'| /'-. .-',--.| .-. || \ Tracktion Software
5 | | | | \ '-' \ `--.| \ \ | | | |' '-' '| || | Corporation
6 `---' `--' `--`--'`---'`--'`--' `---' `--' `---' `--''--' www.tracktion.com
7
8 Tracktion Engine uses a GPL/commercial licence - see LICENCE.md for details.
9*/
10
11namespace tracktion { inline namespace engine
12{
13
15{
16 const char* name;
17 double beatFraction;
18 int snapLevel;
19 bool isTriplets;
20};
21
22static const QuantisationTypeInfo quantisationTypes[] =
23{
24 { NEEDS_TRANS("(none)"), 0.0, 0, false },
25 { NEEDS_TRANS("1/64 beat"), 1.0 / 64.0, 3, false },
26 { NEEDS_TRANS("1/32 beat"), 1.0 / 32.0, 4, false },
27 { NEEDS_TRANS("1/24 beat"), 1.0 / 24.0, 4, true },
28 { NEEDS_TRANS("1/16 beat"), 1.0 / 16.0, 5, false },
29 { NEEDS_TRANS("1/12 beat"), 1.0 / 12.0, 5, true },
30 { NEEDS_TRANS("1/9 beat"), 1.0 / 9.0, 6, true },
31 { NEEDS_TRANS("1/8 beat"), 1.0 / 8.0, 6, false },
32 { NEEDS_TRANS("1/6 beat"), 1.0 / 6.0, 7, true },
33 { NEEDS_TRANS("1/4 beat"), 1.0 / 4.0, 7, false },
34 { NEEDS_TRANS("1/3 beat"), 1.0 / 3.0, 8, true },
35 { NEEDS_TRANS("1/2 beat"), 1.0 / 2.0, 8, false },
36 { NEEDS_TRANS("1 beat"), 1.0, 9, false }
37};
38
39
40//==============================================================================
41QuantisationType::QuantisationType() : state (IDs::QUANTISATION)
42{
43 initialiseCachedValues (nullptr);
44}
45
46QuantisationType::QuantisationType (const juce::ValueTree& vt, juce::UndoManager* um) : state (vt)
47{
48 initialiseCachedValues (um);
49}
50
51QuantisationType::~QuantisationType()
52{
53 state.removeListener (this);
54}
55
56QuantisationType::QuantisationType (const QuantisationType& other)
57 : state (other.state.createCopy()),
58 typeIndex (other.typeIndex),
59 fractionOfBeat (other.fractionOfBeat)
60{
61 initialiseCachedValues (nullptr);
62}
63
64QuantisationType& QuantisationType::operator= (const QuantisationType& other)
65{
66 proportion = other.proportion.get();
67 typeName = other.typeName.get();
68 typeIndex = other.typeIndex;
69 fractionOfBeat = other.fractionOfBeat;
70
71 return *this;
72}
73
74bool QuantisationType::operator== (const QuantisationType& o) const
75{
76 return proportion == o.proportion
77 && typeIndex == o.typeIndex
78 && fractionOfBeat == o.fractionOfBeat
79 && quantiseNoteOffs == o.quantiseNoteOffs;
80}
81
82void QuantisationType::initialiseCachedValues (juce::UndoManager* um)
83{
84 state.addListener (this);
85
86 typeName.referTo (state, IDs::type, um);
87 proportion.referTo (state, IDs::amount, um, 1.0f);
88 quantiseNoteOffs.referTo (state, IDs::quantiseNoteOffs, um, true);
89
90 updateType();
91}
92
93void QuantisationType::updateType()
94{
95 auto typeNameToIndex = [] (const juce::String& newTypeName) -> int
96 {
97 int newType = 0;
98 auto targetFraction = newTypeName.retainCharacters ("0123456789/");
99
100 for (int i = juce::numElementsInArray (quantisationTypes); --i >= 0;)
101 if (targetFraction == juce::String (quantisationTypes[i].name).retainCharacters ("0123456789/"))
102 return i;
103
104 return newType;
105 };
106
107 typeIndex = typeNameToIndex (typeName);
108 updateFraction();
109}
110
111void QuantisationType::updateFraction()
112{
113 if (typeIndex >= 0 && typeIndex < juce::numElementsInArray (quantisationTypes))
114 fractionOfBeat = quantisationTypes[typeIndex].beatFraction;
115 else
116 fractionOfBeat = 0.0;
117}
118
119juce::String QuantisationType::getType (bool translated) const
120{
121 if (typeIndex >= 0 && typeIndex < juce::numElementsInArray (quantisationTypes))
122 return translated ? TRANS(quantisationTypes[typeIndex].name)
123 : quantisationTypes[typeIndex].name;
124
126 return {};
127}
128
129int QuantisationType::getTimecodeSnapTypeLevel (bool& isTriplet) const noexcept
130{
131 if (! juce::isPositiveAndBelow (typeIndex, juce::numElementsInArray (quantisationTypes)))
132 {
134 isTriplet = false;
135 return 0;
136 }
137
138 isTriplet = quantisationTypes[typeIndex].isTriplets;
139
140 return quantisationTypes[typeIndex].snapLevel;
141}
142
144{
145 return typeIndex != 0 && proportion > 0.0f;
146}
147
148void QuantisationType::setProportion (float prop)
149{
150 proportion = juce::jlimit (0.0f, 1.0f, prop);
151}
152
153TimePosition QuantisationType::roundToNearest (TimePosition time, const Edit& edit) const
154{
155 return roundTo (time, 0.5 - 1.0e-10, edit);
156}
157
158TimePosition QuantisationType::roundUp (TimePosition time, const Edit& edit) const
159{
160 return roundTo (time, 1.0 - 1.0e-10, edit);
161}
162
163BeatPosition QuantisationType::roundToBeat (BeatPosition beatNumber, double adjustment) const
164{
165 if (typeIndex == 0)
166 return beatNumber;
167
168 const double beats = fractionOfBeat * floor (beatNumber.inBeats() / fractionOfBeat + adjustment);
169
170 return BeatPosition::fromBeats (proportion == 1.0 ? beats
171 : beatNumber.inBeats() + proportion * (beats - beatNumber.inBeats()));
172}
173
174BeatPosition QuantisationType::roundBeatToNearest (BeatPosition beatNumber) const
175{
176 return roundToBeat (beatNumber, 0.5);
177}
178
179BeatPosition QuantisationType::roundBeatUp (BeatPosition beatNumber) const
180{
181 return roundToBeat (beatNumber, 1.0 - 1.0e-10);
182}
183
184BeatPosition QuantisationType::roundBeatToNearestNonZero (BeatPosition beatNumber) const
185{
186 auto t = roundBeatToNearest (beatNumber);
187
188 return t == BeatPosition() ? BeatPosition::fromBeats (fractionOfBeat) : t;
189}
190
191TimePosition QuantisationType::roundTo (TimePosition time, double adjustment, const Edit& edit) const
192{
193 if (typeIndex == 0)
194 return time;
195
196 auto& s = edit.tempoSequence;
197 auto beats = s.toBeats (time);
198
199 beats = BeatPosition::fromBeats (fractionOfBeat * std::floor (beats.inBeats() / fractionOfBeat + adjustment));
200
201 return proportion >= 1.0 ? s.toTime (beats)
202 : time + toDuration (s.toTime (beats) - toDuration (time)) * proportion.get();
203}
204
205juce::StringArray QuantisationType::getAvailableQuantiseTypes (bool translated)
206{
208
209 for (int i = 0; i < juce::numElementsInArray (quantisationTypes); ++i)
210 s.add (translated ? TRANS(quantisationTypes[i].name)
211 : quantisationTypes[i].name);
212
213 return s;
214}
215
216juce::String QuantisationType::getDefaultType (bool translated)
217{
218 return translated ? TRANS(quantisationTypes[0].name)
219 : quantisationTypes[0].name;
220}
221
222void QuantisationType::valueTreePropertyChanged (juce::ValueTree& vt, const juce::Identifier& i)
223{
224 if (vt == state)
225 {
226 if (i == IDs::type)
227 {
228 typeName.forceUpdateOfCachedValue();
229 updateType();
230 }
231 else if (i == IDs::amount)
232 {
233 updateFraction();
234 }
235 }
236}
237
238void QuantisationType::applyQuantisationToSequence (juce::MidiMessageSequence& ms, Edit& ed, TimePosition start)
239{
240 if (! isEnabled())
241 return;
242
243 for (int i = ms.getNumEvents(); --i >= 0;)
244 {
245 auto* e = ms.getEventPointer (i);
246 auto& m = e->message;
247
248 if (m.isNoteOn())
249 {
250 const auto noteOnTime = (roundToNearest (start + TimeDuration::fromSeconds (m.getTimeStamp()), ed) - start).inSeconds();
251
252 if (auto noteOff = e->noteOffObject)
253 {
254 auto& mOff = noteOff->message;
255
256 if (quantiseNoteOffs)
257 {
258 auto noteOffTime = (roundUp (start + TimeDuration::fromSeconds (mOff.getTimeStamp()), ed) - start).inSeconds();
259
260 static constexpr double beatsToBumpUpBy = 1.0 / 512.0;
261
262 if (noteOffTime <= noteOnTime) // Don't want note on and off time the same
263 noteOffTime = roundUp (TimePosition::fromSeconds (noteOnTime + beatsToBumpUpBy), ed).inSeconds();
264
265 mOff.setTimeStamp (noteOffTime);
266 }
267 else
268 {
269 mOff.setTimeStamp (noteOnTime + (mOff.getTimeStamp() - m.getTimeStamp()));
270 }
271 }
272
273 m.setTimeStamp (noteOnTime);
274 }
275 else if (m.isNoteOff() && quantiseNoteOffs)
276 {
277 m.setTimeStamp ((roundUp (start + TimeDuration::fromSeconds (m.getTimeStamp()), ed) - start).inSeconds());
278 }
279 }
280}
281
282#if TRACKTION_UNIT_TESTS && ENGINE_UNIT_TESTS_QUANTISATION_TYPE
283
284class QuantisationTypeTests : public juce::UnitTest
285{
286public:
287 QuantisationTypeTests()
288 : juce::UnitTest ("QuantisationType", "tracktion_engine")
289 {
290 }
291
292 void runTest() override
293 {
294 auto& engine = *Engine::getEngines()[0];
295 auto edit = Edit::createSingleTrackEdit (engine);
296 auto track = getAudioTracks (*edit)[0];
297 auto c = insertMIDIClip (*track, {}, { 0_tp , 1_tp });
298 c->setEnd (edit->tempoSequence.toTime (2_bp), true);
299
300 MidiList originalList;
301 originalList.addNote (60, 0_bp, 0.25_bd, 127, 0, nullptr);
302 originalList.addNote (61, 0.75_bp, 0.25_bd, 127, 0, nullptr);
303 originalList.addNote (62, 1.25_bp, 0.25_bd, 127, 0, nullptr);
304 originalList.addNote (63, 1.75_bp, 0.25_bd, 127, 0, nullptr);
305
306 auto& list = c->getSequence();
307 list.copyFrom (originalList, {});
308
309 beginTest ("No quantise");
310 {
311 auto& q = c->getQuantisation();
312 auto notes = list.getNotes();
313 expectEquals (q.roundBeatToNearest (notes[0]->getStartBeat()), 0_bp);
314 expectEquals (q.roundBeatToNearest (notes[1]->getStartBeat()), 0.75_bp);
315 expectEquals (q.roundBeatToNearest (notes[2]->getStartBeat()), 1.25_bp);
316 expectEquals (q.roundBeatToNearest (notes[3]->getStartBeat()), 1.75_bp);
317 }
318
319 beginTest ("1/2 bar");
320 {
321 // Apply the quantise directly to the notes
322 QuantisationType q;
323 q.setType ("1/2");
324
325 {
326 auto notes = list.getNotes();
327 expectEquals (q.roundBeatToNearest (notes[0]->getStartBeat()), 0_bp);
328 expectEquals (q.roundBeatToNearest (notes[1]->getStartBeat()), 1.0_bp);
329 expectEquals (q.roundBeatToNearest (notes[2]->getStartBeat()), 1.5_bp);
330 expectEquals (q.roundBeatToNearest (notes[3]->getStartBeat()), 2.0_bp);
331 }
332
333 // Apply quatisation to note starts
334 for (auto n : list.getNotes())
335 n->setStartAndLength (q.roundBeatToNearest (n->getStartBeat()),
336 n->getLengthBeats(), nullptr);
337
338 {
339 c->setLoopRangeBeats ({ 0_bp, 2_bd });
340 c->setEnd (edit->tempoSequence.toTime (4_bp), true);
341
342 auto notes = c->getSequenceLooped().getNotes();
343 expectEquals (notes.size(), 6);
344
345 expectEquals (notes[0]->getStartBeat(), 0_bp);
346 expectEquals (notes[1]->getStartBeat(), 1.0_bp);
347 expectEquals (notes[2]->getStartBeat(), 1.5_bp);
348 expectEquals (notes[3]->getStartBeat(), 2.0_bp);
349 expectEquals (notes[4]->getStartBeat(), 3.0_bp);
350 expectEquals (notes[5]->getStartBeat(), 3.5_bp);
351 }
352 }
353
354 beginTest ("1 bar");
355 {
356 // Reset the list to the original
357 list.copyFrom (originalList, {});
358
359 // Apply the quantise directly to the notes
360 QuantisationType q;
361 q.setType ("1");
362
363 // Apply quatisation to note starts
364 for (auto n : list.getNotes())
365 n->setStartAndLength (q.roundBeatToNearest (n->getStartBeat()),
366 n->getLengthBeats(), nullptr);
367
368 {
369 auto notes = c->getSequenceLooped().getNotes();
370 expectEquals (notes.size(), 6);
371
372 expectEquals (notes[0]->getStartBeat(), 0_bp);
373 expectEquals (notes[1]->getStartBeat(), 1.0_bp);
374 expectEquals (notes[2]->getStartBeat(), 1.0_bp);
375 expectEquals (notes[3]->getStartBeat(), 2.0_bp);
376 expectEquals (notes[4]->getStartBeat(), 3.0_bp);
377 expectEquals (notes[5]->getStartBeat(), 3.0_bp);
378 }
379 }
380 }
381};
382
383static QuantisationTypeTests quantisationTypeTests;
384
385#endif
386
387}} // namespace tracktion { inline namespace engine
void forceUpdateOfCachedValue()
void referTo(ValueTree &tree, const Identifier &property, UndoManager *um)
Type get() const noexcept
MidiEventHolder * getEventPointer(int index) const noexcept
int getNumEvents() const noexcept
void add(String stringToAdd)
String retainCharacters(StringRef charactersToRetain) const
void addListener(Listener *listener)
void removeListener(Listener *listener)
The Tracktion Edit class!
bool isEnabled() const
this type may represent "no quantising"
int getTimecodeSnapTypeLevel(bool &isTriplet) const noexcept
Returns the TimecodeSnapType level for the current quantisation type.
T floor(T... args)
floor
#define TRANS(stringLiteral)
#define NEEDS_TRANS(stringLiteral)
#define jassertfalse
Type jlimit(Type lowerLimit, Type upperLimit, Type valueToConstrain) noexcept
bool isPositiveAndBelow(Type1 valueToTest, Type2 upperLimit) noexcept
constexpr int numElementsInArray(Type(&)[N]) noexcept
juce::Array< AudioTrack * > getAudioTracks(const Edit &edit)
Returns all the AudioTracks in an Edit.
MidiClip::Ptr insertMIDIClip(ClipOwner &parent, const juce::String &name, TimeRange position)
Inserts a new MidiClip into the ClipOwner's clip list.
constexpr TimeDuration toDuration(TimePosition)
Converts a TimePosition to a TimeDuration.
Represents a position in real-life time.
constexpr double inSeconds() const
Returns the TimePosition as a number of seconds.
time