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

« « « Anklang Documentation
Loading...
Searching...
No Matches
tracktion_WarpTimeManager.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
14inline HashCode hashDouble (double d) noexcept
15{
16 static_assert (sizeof (double) == sizeof (int64_t), "double and int64 different sizes");
17 union Val { double asDouble; int64_t asInt; } v;
18 v.asDouble = d;
19 return v.asInt;
20}
21
22HashCode WarpMarker::getHash() const noexcept { return hashDouble (sourceTime.inSeconds()) ^ hashDouble (warpTime.inSeconds()); }
23
24template <typename FloatingPointType>
26{
27 void process (const FloatingPointType* inputSamples, int numSamples, FloatingPointType* outputSamples)
28 {
29 for (int i = 0; i < numSamples; ++i)
30 {
31 FloatingPointType currentSample = inputSamples[i];
32 outputSamples[i] = currentSample - lastSample;
33 lastSample = currentSample;
34 }
35 }
36
37 FloatingPointType lastSample { (FloatingPointType) 0.0 };
38};
39
40//==============================================================================
42{
43 struct Config
44 {
45 float sensitivity;
46 };
47
48 static Ptr getOrCreateDetectionJob (Engine& e, const AudioFile& file, Config config)
49 {
51
52 for (auto j : jobs)
53 if (auto tdj = dynamic_cast<TransientDetectionJob*> (j))
54 if (tdj->isIdenticalTo (file, config))
55 return j;
56
57 return new TransientDetectionJob (e, file, config);
58 }
59
60 bool isIdenticalTo (const AudioFile& f, Config c)
61 {
62 return file == f && config.sensitivity == c.sensitivity;
63 }
64
65 juce::Array<TimePosition> getTimes() const { return transientTimes; }
66
67protected:
68 bool setUpRender() override { return reader != nullptr && totalNumSamples > 0; }
69
70 bool completeRender() override
71 {
72 if (transientTimes.size() > 1)
73 {
74 auto trimTransients = [this]() -> bool
75 {
76 const auto minTime = 0.1s;
77 auto lastTime = transientTimes.getLast();
78 const int initialSize = transientTimes.size();
79
80 for (int i = transientTimes.size() - 1; --i >= 0;)
81 {
82 const auto t = transientTimes.getUnchecked (i);
83
84 if ((lastTime - t) < minTime)
85 transientTimes.remove (i);
86 else
87 lastTime = t;
88 }
89
90 return initialSize != transientTimes.size();
91 };
92
93 for (int i = 10; --i >= 0;)
94 if (! trimTransients())
95 break;
96 }
97
98 return true;
99 }
100
101 bool renderNextBlock() override
102 {
104 auto numLeft = totalNumSamples - numSamplesRead;
105 auto numToDo = (int) std::min ((SampleCount) 32768, numLeft);
106
107 AudioScratchBuffer scratch (numChannels, numToDo);
108 auto schannels = juce::AudioChannelSet::canonicalChannelSet(numChannels);
109 reader->readSamples (numToDo, scratch.buffer, schannels, 0, juce::AudioChannelSet::stereo(), 5000);
110
111 if (findingNormaliseLevel)
112 processNextNormaliseBuffer (scratch.buffer);
113 else
114 processNextBuffer (scratch.buffer);
115
116 numSamplesRead += numToDo;
117 progress = (numSamplesRead / (float) totalNumSamples)
118 * (findingNormaliseLevel ? 0.5f : 1.0f);
119
120 if (findingNormaliseLevel && numSamplesRead >= totalNumSamples)
121 {
122 auto peak = std::max (std::abs (fileMinMax.getStart()),
123 std::abs (fileMinMax.getEnd()));
124 normaliseScale = peak > 0.0f ? 1.0f / peak : 1.0f;
125 reader->setReadPosition (0);
126 numSamplesRead = 0;
127 findingNormaliseLevel = false;
128 }
129
130 return ! findingNormaliseLevel && numSamplesRead >= totalNumSamples;
131 }
132
133private:
134 AudioFile file;
135 Config config;
136
137 SampleCount numSamplesRead = 0, totalNumSamples = 0;
138 int numChannels = 0;
139 double sampleRate = 0;
141
142 AudioFileUtils::EnvelopeFollower envelopeFollower[3];
143 Differentiator<float> differentiator;
144 juce::Array<TimePosition> transientTimes;
145
146 juce::Range<float> fileMinMax;
147 float normaliseScale = -1.0f;
148 const float thresh = juce::Decibels::decibelsToGain (-25.0f);
149 int triggerTimer = 0;
150 int countDownTimer = 0;
151 bool findingNormaliseLevel = true;
152
153 TransientDetectionJob (Engine& e, const AudioFile& af, Config c)
154 : Job (e, AudioFile (e)), file (af), config (c),
155 totalNumSamples (af.getLengthInSamples()),
156 numChannels (af.getNumChannels()),
157 reader (e.getAudioFileManager().cache.createReader (af))
158 {
159 TRACKTION_ASSERT_MESSAGE_THREAD
160 // N.B. The argumnet to the Job constructor is the proxy file to use
161 // Don't send the audio file here or it will get deleted!
162 jassert (proxy.isNull());
163
164 if (reader != nullptr)
165 sampleRate = reader->getSampleRate();
166
167 triggerTimer = int (sampleRate * 50 * 0.001); // 50ms - should be linked to BPM
168 envelopeFollower[0].setCoefficients (1.0, 0.002f);
169 envelopeFollower[1].setCoefficients (1.0, 0.002f);
170 envelopeFollower[2].setCoefficients (1.0, 0.002f);
171 }
172
173 void processNextNormaliseBuffer (const juce::AudioBuffer<float>& buffer)
174 {
175 fileMinMax = fileMinMax.getUnionWith (juce::FloatVectorOperations::findMinAndMax (buffer.getReadPointer (0),
176 buffer.getNumSamples()));
177 }
178
179 void processNextBuffer (const juce::AudioBuffer<float>& buffer)
180 {
181 const int numSamples = buffer.getNumSamples();
182 AudioScratchBuffer scratch (1, numSamples);
183 auto& detectionBuffer = scratch.buffer;
184
185 detectionBuffer.copyFrom (0, 0, buffer, 0, 0, numSamples);
186 detectionBuffer.applyGain (0, 0, numSamples, normaliseScale);
187 float* data = detectionBuffer.getWritePointer (0);
188
189 envelopeFollower[0].processEnvelope (data, data, numSamples);
190 envelopeFollower[1].processEnvelope (data, data, numSamples);
191
192 differentiator.process (data, numSamples, data);
193 envelopeFollower[2].processEnvelope (data, data, numSamples);
194
195 for (int i = 0; i < numSamples; i++)
196 {
197 const float sample = *data++;
198
199 if (countDownTimer)
200 countDownTimer--;
201
202 if (sample > thresh)
203 {
204 if (countDownTimer == 0)
205 {
206 int rewindIndex = int (i - sampleRate * 0.0005); //rewind by 0.05ms
207
208 if (rewindIndex < 0)
209 rewindIndex = 0;
210
211 transientTimes.add (sampleToSeconds (numSamplesRead + rewindIndex));
212 }
213
214 countDownTimer = triggerTimer;
215 }
216 }
217 }
218
219 TimePosition sampleToSeconds (SampleCount sample) const
220 {
221 return TimePosition::fromSeconds (sampleRate > 0.0 ? sample / sampleRate : 0.0);
222 }
223
225};
226
227//==============================================================================
229 : edit (c.edit), clip (&c), sourceFile (c.edit.engine)
230{
231 state = c.state.getOrCreateChildWithName (IDs::WARPTIME, &edit.getUndoManager());
232 auto markersTree = state.getOrCreateChildWithName (IDs::WARPMARKERS, &edit.getUndoManager());
233 markers = std::make_unique<WarpMarkerList> (markersTree);
234
235 const auto clipLen = toPosition (TimeDuration::fromSeconds (AudioFile (c.edit.engine, clip->getOriginalFile()).getLength()));
236
237 // If this is the first time that we've built the Manager
238 if (markers->isEmpty())
239 {
241 insertMarker (WarpMarker (clipLen, clipLen));
242 setWarpEndMarkerTime (clipLen);
243 }
244
245 editLoadedCallback = std::make_unique<Edit::LoadFinishedCallback<WarpTimeManager>> (*this, edit);
246
247 edit.engine.getWarpTimeFactory().addWarpTimeManager (*this);
248}
249
251 : edit (e), sourceFile (f), endMarkerEnabled (false), endMarkersLimited (true)
252{
253 state = parentTree.getOrCreateChildWithName (IDs::WARPTIME, &edit.getUndoManager());
254 auto markersTree = state.getOrCreateChildWithName (IDs::WARPMARKERS, &edit.getUndoManager());
255 markers = std::make_unique<WarpMarkerList> (markersTree);
256
257 setSourceFile (f);
258
259 edit.engine.getWarpTimeFactory().addWarpTimeManager (*this);
260}
261
263{
264 if (transientDetectionJob != nullptr)
265 transientDetectionJob->removeListener (this);
266
267 transientDetectionJob = nullptr;
268 edit.engine.getWarpTimeFactory().removeWarpTimeManager (*this);
269}
270
272{
273 jassert (clip == nullptr);
274 sourceFile = af;
275
276 if (markers->isEmpty())
277 {
278 const auto clipLen = toPosition (TimeDuration::fromSeconds (sourceFile.getLength()));
279
280 if (sourceFile.isValid())
281 {
283 insertMarker (WarpMarker (clipLen, clipLen));
284 setWarpEndMarkerTime (clipLen);
285
286 editLoadedCallback = std::make_unique<Edit::LoadFinishedCallback<WarpTimeManager>> (*this, edit);
287 }
288 }
289}
290
292{
293 return clip != nullptr ? AudioFile (clip->edit.engine, clip->getOriginalFile()) : sourceFile;
294}
295
297{
298 return TimeDuration::fromSeconds (getSourceFile().getLength());
299}
300
302{
303 int index = 0;
304
305 while (index < markers->objects.size() && markers->objects.getUnchecked (index)->warpTime < marker.warpTime)
306 index++;
307
308 auto v = createValueTree (IDs::WARPMARKER,
309 IDs::sourceTime, marker.sourceTime.inSeconds(),
310 IDs::warpTime, marker.warpTime.inSeconds());
311
312 markers->state.addChild (v, index, getUndoManager());
313
314 return index;
315}
316
318{
319 if (index == 0 || index == markers->size() - 1)
320 moveMarker (index, markers->objects.getUnchecked (index)->sourceTime);
321 else
322 markers->state.removeChild (index, getUndoManager());
323}
324
326{
327 markers->state.removeAllChildren (getUndoManager());
328 const auto clipLen = toPosition (getSourceLength());
329
331 insertMarker (WarpMarker (clipLen, clipLen));
332 setWarpEndMarkerTime (clipLen);
333}
334
336{
338 auto m = markers->state.getChild (index);
339
340 if (! m.isValid())
341 return newWarpTime;
342
343 if (index > 0)
344 {
345 WarpMarker* a = markers->objects.getUnchecked (index - 1);
346 WarpMarker* b = markers->objects.getUnchecked (index);
347 auto srcLen = b->sourceTime - a->sourceTime;
348 double stretchRatio = (newWarpTime - a->warpTime) / srcLen;
349
350 if (stretchRatio < 0.10001)
351 newWarpTime = a->warpTime + srcLen * 0.10001;
352 else if (stretchRatio > 19.9999)
353 newWarpTime = a->warpTime + srcLen * 19.9999;
354 }
355
356 if (index < markers->objects.size() - 1)
357 {
358 WarpMarker* a = markers->objects.getUnchecked (index);
359 WarpMarker* b = markers->objects.getUnchecked (index + 1);
360 auto srcLen = b->sourceTime - a->sourceTime;
361 double stretchRatio = (b->warpTime - newWarpTime) / srcLen;
362
363 if (stretchRatio < 0.10001)
364 newWarpTime = b->warpTime - srcLen * 0.10001;
365 else if (stretchRatio > 19.9999)
366 newWarpTime = b->warpTime - srcLen * 19.9999;
367 }
368
369 if (endMarkersLimited && (index == 0 || (index == markers->objects.size() - 1)))
370 newWarpTime = juce::jlimit (TimePosition(), toPosition (getSourceLength()), newWarpTime);
371
372 m.setProperty (IDs::warpTime, newWarpTime.inSeconds(), getUndoManager());
373
374 return newWarpTime;
375}
376
378{
379 if (endTime > 0.0s)
380 state.setProperty (IDs::warpEndMarkerTime, endTime.inSeconds(), &edit.getUndoManager());
381}
382
383juce::Array<TimeRange> WarpTimeManager::getWarpTimeRegions (const TimeRange overallTimeRegion) const
384{
385 juce::Array<TimeRange> visibleWarpRegions;
386 auto& markersArray = markers->objects;
387
388 if (markersArray.isEmpty())
389 {
390 visibleWarpRegions.add (overallTimeRegion);
391 return visibleWarpRegions;
392 }
393
394 auto timeRegion = overallTimeRegion;
395 TimeDuration overallTime;
396 auto warpedClipLength = getWarpedEnd();
397
398 // trim this region to the end of the clip content.
399 if (timeRegion.getEnd() > warpedClipLength)
400 timeRegion = timeRegion.withEnd (warpedClipLength);
401
402 //set up the warp regions
403 TimeRange warpRegion (overallTimeRegion.getStart(), warpedClipLength);
404
405 for (int markerIndex = 0; markerIndex <= markersArray.size(); markerIndex++)
406 {
407 if (markerIndex == markersArray.size()) // if we're on the last region
408 warpRegion = warpRegion.withEnd (std::max (warpRegion.getStart(), warpedClipLength));
409 else
410 warpRegion = warpRegion.withEnd (std::max (warpRegion.getStart(), markersArray.getUnchecked (markerIndex)->warpTime));
411
412 auto warpRegionConstrained = timeRegion.getIntersectionWith (warpRegion);
413
414 if (warpRegionConstrained.getLength() > 0s) // don't add zero length regions
415 {
416 visibleWarpRegions.add (warpRegionConstrained);
417 overallTime = overallTime + warpRegionConstrained.getLength();
418 }
419
420 warpRegion = warpRegion.withStart (warpRegion.getEnd());
421 }
422
423 return visibleWarpRegions;
424}
425
427{
428 auto& markersArray = markers->objects;
429
430 if (markersArray.isEmpty())
431 return warpTime;
432
433 WarpMarker startMarker, endMarker;
434
435 auto first = *markersArray.getFirst();
436 auto last = *markersArray.getLast();
437
438 if (warpTime <= first.warpTime) //below or on the 1st marker
439 {
440 startMarker = {};
441 endMarker = first;
442 }
443 else if (warpTime > last.warpTime) // after the last marker
444 {
445 startMarker = last;
446 auto sourceLen = toPosition (clip->getSourceLength());
447 endMarker = WarpMarker (sourceLen, sourceLen);
448 }
449 else
450 {
451 int index = 0;
452 auto numMarkers = markersArray.size();
453
454 while (index < numMarkers && markersArray.getUnchecked (index)->warpTime < warpTime)
455 index++;
456
457 if (index > 0)
458 startMarker = *markersArray.getUnchecked (index - 1);
459
460 endMarker = *markersArray.getUnchecked (index);
461 }
462
463 const WarpMarker markerRanges (toPosition (endMarker.sourceTime - startMarker.sourceTime),
464 toPosition (endMarker.warpTime - startMarker.warpTime));
465
466 TimePosition sourcePosition;
467
468 if (markerRanges.warpTime == 0.0s)
469 {
470 sourcePosition = 0.0s;
471 }
472 else
473 {
474 const double warpProportion = (warpTime - startMarker.warpTime) / toDuration (markerRanges.warpTime);
475 sourcePosition = (markerRanges.sourceTime * warpProportion) + toDuration (startMarker.sourceTime);
476 }
477
478 return sourcePosition;
479}
480
482{
483 auto& markersArray = markers->objects;
484
485 if (markersArray.isEmpty())
486 return sourceTime;
487
488 WarpMarker* before = nullptr;
489 WarpMarker* after = nullptr;
490
491 for (auto wm : markersArray)
492 {
493 before = after;
494 after = wm;
495
496 if (before != nullptr && after != nullptr
497 && (sourceTime >= before->sourceTime && sourceTime <= after->sourceTime))
498 break;
499 }
500
501 TimeRange source (before == nullptr ? TimePosition() : before->sourceTime,
502 after == nullptr ? toPosition (getSourceLength()) : after->sourceTime);
503
504 if (source.getLength() == 0.0s)
505 return sourceTime;
506
507 auto prop = (sourceTime - source.getStart()) / source.getLength();
508
509 TimeRange warped (before == nullptr ? TimePosition() : before->warpTime,
510 after == nullptr ? getWarpedEnd() : after->warpTime);
511
512 return warped.getStart() + (warped.getLength() * prop);
513}
514
516{
517 jassert (markers->size() != 0);
518
519 return markers->objects.getFirst()->warpTime;
520}
521
523{
524 jassert (markers->size() != 0);
525
526 return markers->objects.getLast()->warpTime;
527}
528
530{
531 HashCode h = 0;
532
533 for (auto wm : markers->objects)
534 h ^= wm->getHash();
535
536 h ^= hashDouble (getWarpEndMarkerTime().inSeconds());
537
538 return h;
539}
540
542{
544 return TimePosition::fromSeconds (state.getProperty (IDs::warpEndMarkerTime, 0.0));
545
546 return toPosition (getSourceLength());
547}
548
549void WarpTimeManager::editFinishedLoading()
550{
552 config.sensitivity = 0.5f;
553 transientDetectionJob = TransientDetectionJob::getOrCreateDetectionJob (edit.engine, getSourceFile(), config);
554
555 if (transientDetectionJob != nullptr)
556 transientDetectionJob->addListener (this);
557
558 editLoadedCallback = nullptr;
559}
560
561juce::UndoManager* WarpTimeManager::getUndoManager() const
562{
563 return &edit.getUndoManager();
564}
565
566void WarpTimeManager::jobFinished (RenderManager::Job& job, bool /*completedOk*/)
567{
568 if (auto tdj = dynamic_cast<TransientDetectionJob*> (&job))
569 {
570 transientTimes.second = tdj->getTimes();
571 transientTimes.first = true;
572 }
573
574 job.removeListener (this);
575 transientDetectionJob = nullptr;
576}
577
578//==============================================================================
580{
581 {
582 const juce::ScopedLock sl (warpTimeLock);
583
584 for (auto c : warpTimeManagers)
585 if (c->clip == &clip)
586 return c;
587 }
588
589 if (auto wac = const_cast<WaveAudioClip*> (dynamic_cast<const WaveAudioClip*> (&clip)))
590 return new WarpTimeManager (*wac);
591
593 return {};
594}
595
596void WarpTimeFactory::addWarpTimeManager (WarpTimeManager& wtm)
597{
598 const juce::ScopedLock sl (warpTimeLock);
599 jassert (! warpTimeManagers.contains (&wtm));
600 warpTimeManagers.addIfNotAlreadyThere (&wtm);
601}
602
603void WarpTimeFactory::removeWarpTimeManager (WarpTimeManager& wtm)
604{
605 const juce::ScopedLock sl (warpTimeLock);
606 jassert (warpTimeManagers.contains (&wtm));
607 warpTimeManagers.removeAllInstancesOf (&wtm);
608}
609
610}} // namespace tracktion { inline namespace engine
void add(const ElementType &newElement)
int getNumSamples() const noexcept
const Type * getReadPointer(int channelNumber) const noexcept
static AudioChannelSet JUCE_CALLTYPE stereo()
static AudioChannelSet JUCE_CALLTYPE canonicalChannelSet(int numChannels)
static Type decibelsToGain(Type decibels, Type minusInfinityDb=Type(defaultMinusInfinitydB))
bool isValid() const noexcept
constexpr ValueType getStart() const noexcept
constexpr ValueType getEnd() const noexcept
constexpr Range getUnionWith(Range other) const noexcept
ValueTree & setProperty(const Identifier &name, const var &newValue, UndoManager *undoManager)
const var & getProperty(const Identifier &name) const noexcept
ValueTree getOrCreateChildWithName(const Identifier &type, UndoManager *undoManager)
Base class for Clips that produce some kind of audio e.g.
virtual juce::File getOriginalFile() const =0
Must return the file that the source ProjectItemID refers to.
virtual TimeDuration getSourceLength() const =0
Must return the length in seconds of the source material e.g.
void setCoefficients(float attack, float release) noexcept
Sets the times for the vaious stages of the envelope.
An audio scratch buffer that has pooled storage.
juce::AudioBuffer< float > & buffer
The buffer to use.
A clip in an edit.
The Tracktion Edit class!
juce::UndoManager & getUndoManager() noexcept
Returns the juce::UndoManager used for this Edit.
Engine & engine
A reference to the Engine.
The Engine is the central class for all tracktion sessions.
RenderManager & getRenderManager() const
Returns the RenderManager instance.
WarpTimeFactory & getWarpTimeFactory() const
Returns the WarpTimeFactory instance.
The base class that all generator jobs derive from.
juce::ReferenceCountedArray< Job > getRenderJobsWithoutCreating(const AudioFile &)
Returns all the jobs that may be processing the given file.
WarpTimeManager::Ptr getWarpTimeManager(const Clip &)
Returns a WarpTimeManager for a given clip.
A WarpTimeManager contains a list of WarpMarkers and some source material and maps times from a linea...
void removeAllMarkers()
Removes all WarpMarkers.
TimePosition getWarpedEnd() const
Returns the endTime of the entire warped region.
juce::Array< TimeRange > getWarpTimeRegions(TimeRange overallTimeRegion) const
Time region can be longer than the clip and the returned array will loop over the clip to match the l...
bool isWarpEndMarkerEnabled() const noexcept
Returns true if the end marker is being used as the end of the source material.
TimePosition moveMarker(int index, TimePosition newWarpTime)
Moves a WarpMarker at a given index to a new time.
HashCode getHash() const
Returns a hash representing this warp list.
WarpTimeManager(AudioClipBase &)
Creates a WarpTimeManager to warp a clip.
AudioFile getSourceFile() const
Returns the current source file.
void removeMarker(int index)
Removes a WarpMarker at a given index.
TimePosition warpTimeToSourceTime(TimePosition warpTime) const
Converts a warp time (i.e.
void setSourceFile(const AudioFile &)
Sets a source fiel to warp.
void setWarpEndMarkerTime(TimePosition endTime)
Sets the end time of the source material.
int insertMarker(WarpMarker)
Inserts a new WarpMarker.
TimePosition sourceTimeToWarpTime(TimePosition sourceTime) const
Converts a source time (i.e.
TimePosition getWarpEndMarkerTime() const
Sets position in warped region of the redered file end point.
TimePosition getWarpedStart() const
Returns the start time of the warped region (can be -ve)
TimeDuration getSourceLength() const
Returns the length of the source file.
An audio clip that uses an audio file as its source.
T is_pointer_v
#define jassert(expression)
#define JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(className)
#define jassertfalse
typedef int
typedef double
T max(T... args)
T min(T... args)
Type jlimit(Type lowerLimit, Type upperLimit, Type valueToConstrain) noexcept
typedef int64_t
Represents a duration in real-life time.
Represents a position in real-life time.
constexpr double inSeconds() const
Returns the TimePosition as a number of seconds.
bool setUpRender() override
Subclasses should override this to set-up their render process.
bool renderNextBlock() override
During a render process this will be repeatedly called.
bool completeRender() override
This is called once after all the render blocks have completed.
A WarpMarker is a point that maps from a linear "source" time to a "warped" time.
HashCode getHash() const noexcept
Returns a hash for this marker.
#define CRASH_TRACER
This macro adds the current location to a stack which gets logged if a crash happens.