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

« « « Anklang Documentation
Loading...
Searching...
No Matches
tracktion_ClipEffects.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
11#include "../../playback/audionodes/tracktion_AudioNode.h"
12#include "../../playback/audionodes/tracktion_WaveAudioNode.h"
13#include "../../playback/audionodes/tracktion_TrackCompAudioNode.h"
14#include "../../playback/audionodes/tracktion_SpeedRampAudioNode.h"
15#include "../../playback/audionodes/tracktion_PluginAudioNode.h"
16#include "../../playback/audionodes/tracktion_FadeInOutAudioNode.h"
17
18namespace tracktion { inline namespace engine
19{
20
21static inline HashCode hashValueTree (HashCode startHash, const juce::ValueTree& v)
22{
23 startHash ^= v.getType().toString().hashCode64() * (v.getParent().indexOf (v) + 1);
24
25 for (int i = v.getNumProperties(); --i >= 0;)
26 startHash ^= v[v.getPropertyName (i)].toString().hashCode64();
27
28 for (int i = v.getNumChildren(); --i >= 0;)
29 startHash ^= hashValueTree (startHash, v.getChild (i));
30
31 return startHash;
32}
33
34static inline HashCode hashPlugin (const juce::ValueTree& effectState, Plugin& plugin)
35{
37 HashCode h = juce::String (effectState.getParent().indexOf (effectState) + 1).hashCode64();
38
39 for (int param = plugin.getNumAutomatableParameters(); --param >= 0;)
40 {
41 if (auto ap = plugin.getAutomatableParameter (param))
42 {
43 auto& ac = ap->getCurve();
44
45 if (ac.getNumPoints() == 0)
46 {
47 h = (juce::String (h) + juce::String (ap->getCurrentValue())).hashCode64();
48 }
49 else
50 {
51 for (int i = 0; i < ac.getNumPoints(); ++i)
52 {
53 const auto p = ac.getPoint (i);
54 auto pointH = juce::String (p.time.inSeconds()) + juce::String (p.value) + juce::String (p.curve);
55 h = (juce::String (h) + pointH).hashCode64();
56 }
57 }
58 }
59 }
60
61 return h;
62}
63
64//==============================================================================
66 private juce::AsyncUpdater
67{
68 ClipPropertyWatcher (ClipEffects& o) : clipEffects (o), clipState (o.clip.state)
69 {
70 clipState.addListener (this);
71 }
72
73private:
74 ClipEffects& clipEffects;
75 juce::ValueTree clipState;
76
77 void invalidateCache()
78 {
79 clipEffects.cachedClipProperties = nullptr;
80 clipEffects.cachedHash = ClipEffects::hashNeedsRecaching;
81 }
82
83 void valueTreePropertyChanged (juce::ValueTree& v, const juce::Identifier& i) override
84 {
85 if (v == clipState)
86 {
87 using namespace IDs;
88
89 if (matchesAnyOf (i, { start, length, offset }))
90 {
91 invalidateCache();
92 }
93 else if (matchesAnyOf (i, { speed, loopStart, loopLength, loopStartBeats, loopLengthBeats, autoTempo }))
94 {
95 invalidateCache();
97 }
98 }
99 }
100
101 void valueTreeChildAdded (juce::ValueTree&, juce::ValueTree&) override {}
102 void valueTreeChildRemoved (juce::ValueTree&, juce::ValueTree&, int) override {}
103 void valueTreeChildOrderChanged (juce::ValueTree&, int, int) override {}
104 void valueTreeParentChanged (juce::ValueTree&) override {}
105
106 void handleAsyncUpdate() override
107 {
108 clipEffects.invalidateAllEffects();
109 }
110
112};
113
114ClipEffects::ClipEffects (const juce::ValueTree& v, AudioClipBase& c)
115 : ValueTreeObjectList<ClipEffect> (v), clip (c), state (v)
116{
117 rebuildObjects();
118 clipPropertyWatcher = std::make_unique<ClipPropertyWatcher> (*this);
119}
120
121ClipEffects::~ClipEffects()
122{
123 freeObjects();
124}
125
126//==============================================================================
127}} // namespace tracktion { inline namespace engine
128
129namespace juce {
130
131template <>
132struct VariantConverter<tracktion::engine::ClipEffect::EffectType>
133{
134 static tracktion::engine::ClipEffect::EffectType fromVar (const var& v)
135 {
136 using namespace tracktion::engine;
137
138 auto s = v.toString();
139
140 if (s == "volume") return ClipEffect::EffectType::volume;
141 if (s == "fadeInOut") return ClipEffect::EffectType::fadeInOut;
142 if (s == "tapeStartStop") return ClipEffect::EffectType::tapeStartStop;
143 if (s == "stepVolume") return ClipEffect::EffectType::stepVolume;
144 if (s == "pitchShift") return ClipEffect::EffectType::pitchShift;
145 if (s == "warpTime") return ClipEffect::EffectType::warpTime;
146 if (s == "normalise") return ClipEffect::EffectType::normalise;
147 if (s == "makeMono") return ClipEffect::EffectType::makeMono;
148 if (s == "reverse") return ClipEffect::EffectType::reverse;
149 if (s == "invert") return ClipEffect::EffectType::invert;
150 if (s == "filter") return ClipEffect::EffectType::filter;
151
152 return ClipEffect::EffectType::none;
153 }
154
155 static var toVar (const tracktion::engine::ClipEffect::EffectType& t)
156 {
157 using namespace tracktion::engine;
158
159 switch (t)
160 {
161 case ClipEffect::EffectType::none: return {};
162 case ClipEffect::EffectType::volume: return "volume";
163 case ClipEffect::EffectType::fadeInOut: return "fadeInOut";
164 case ClipEffect::EffectType::tapeStartStop: return "tapeStartStop";
165 case ClipEffect::EffectType::stepVolume: return "stepVolume";
166 case ClipEffect::EffectType::pitchShift: return "pitchShift";
167 case ClipEffect::EffectType::warpTime: return "warpTime";
168 case ClipEffect::EffectType::normalise: return "normalise";
169 case ClipEffect::EffectType::makeMono: return "makeMono";
170 case ClipEffect::EffectType::reverse: return "reverse";
171 case ClipEffect::EffectType::invert: return "invert";
172 case ClipEffect::EffectType::filter: return "filter";
173 }
174
175 return {};
176 }
177};
178
179} namespace tracktion { inline namespace engine {
180
181//==============================================================================
183{
185
186 static constexpr SampleCount defaultBlockSize = 1024;
187
189 const AudioFile& dest, const AudioFile& src,
190 SampleCount blockSizeToUse)
191 : engine (e), destination (dest),
192 source (src), blockSize (blockSizeToUse)
193 {
194 }
195
196 virtual ~ClipEffectRenderJob() {}
197
202 virtual bool setUpRender() = 0;
203
207 virtual bool renderNextBlock() = 0;
208
213 virtual bool completeRender() = 0;
214
215 Engine& engine;
216 const AudioFile destination, source;
217 const SampleCount blockSize;
218 std::atomic<float> progress { 0.0f };
219
221};
222
223//==============================================================================
224ClipEffect::ClipEffect (const juce::ValueTree& v, ClipEffects& o)
225 : edit (o.clip.edit), state (v), clipEffects (o), destinationFile (edit.engine)
226{
227 state.addListener (this);
228}
229
230juce::ValueTree ClipEffect::create (EffectType t)
231{
232 return createValueTree (IDs::EFFECT,
234}
235
236void ClipEffect::createEffectAndAddToValueTree (Edit& edit, juce::ValueTree parent, ClipEffect::EffectType effectType, int index)
237{
238 auto& undoManager = edit.getUndoManager();
239
240 if (effectType == EffectType::filter)
241 {
242 if (auto af = edit.engine.getUIBehaviour()
243 .showMenuAndCreatePlugin (Plugin::Type::effectPlugins, edit))
244 {
245 af->setProcessingEnabled (false);
246 af->flushPluginStateToValueTree();
247
248 auto v = createValueTree (IDs::EFFECT,
249 IDs::type, juce::VariantConverter<ClipEffect::EffectType>::toVar (EffectType::filter));
250
251 v.addChild (af->state, -1, nullptr);
252
253 if (parent.isValid())
254 parent.addChild (v, index, &undoManager);
255 }
256 else if (parent.getNumChildren() == 0)
257 {
258 auto state = parent.getParent();
259
260 if (state.isValid())
261 {
262 state.removeChild (parent, &undoManager);
263 state.removeProperty (IDs::effectsVisible, &undoManager);
264 }
265 }
266 }
267 else
268 {
269 if (parent.isValid())
270 parent.addChild (ClipEffect::create (effectType), index, &undoManager);
271 }
272}
273
274juce::String ClipEffect::getTypeDisplayName (EffectType t)
275{
276 switch (t)
277 {
278 case EffectType::volume: return TRANS("Volume");
279 case EffectType::fadeInOut: return TRANS("Fade in/out");
280 case EffectType::tapeStartStop: return TRANS("Tape stop/start");
281 case EffectType::stepVolume: return TRANS("Step volume");
282 case EffectType::pitchShift: return TRANS("Pitch shift");
283 case EffectType::warpTime: return TRANS("Warp time");
284 case EffectType::normalise: return TRANS("Normalise");
285 case EffectType::makeMono: return TRANS("Mono");
286 case EffectType::reverse: return TRANS("Reverse");
287 case EffectType::invert: return TRANS("Phase Invert");
288 case EffectType::filter: return TRANS("Plugin");
289 case EffectType::none:
290 default: return "<" + TRANS ("None") + ">";
291 }
292}
293
294void ClipEffect::addEffectsToMenu (juce::PopupMenu& m)
295{
296 auto addItems = [&m] (juce::StringRef heading, juce::Array<EffectType> t)
297 {
298 m.addSectionHeader (heading);
299
300 for (auto e : t)
301 m.addItem ((int) e, getTypeDisplayName (e));
302 };
303
304 addItems (TRANS("Volume"), { EffectType::fadeInOut, EffectType::stepVolume, EffectType::volume });
305 addItems (TRANS("Time/Pitch"), { EffectType::pitchShift, EffectType::tapeStartStop, EffectType::warpTime });
306 addItems (TRANS("Effects"), { EffectType::filter, EffectType::reverse });
307 addItems (TRANS("Mastering"), { EffectType::makeMono, EffectType::normalise, EffectType::invert });
308}
309
310ClipEffect::EffectType ClipEffect::getType() const
311{
313}
314
315HashCode ClipEffect::getIndividualHash() const
316{
317 return hashValueTree (0, state);
318}
319
320HashCode ClipEffect::getHash() const
321{
322 auto parent = state.getParent();
323 auto index = parent.indexOf (state);
324 HashCode hash = index ^ static_cast<HashCode> (clipEffects.clip.itemID.getRawID());
325
326 for (int i = 0; i <= index; ++i)
327 if (auto ce = clipEffects.getClipEffect (parent.getChild (i)))
328 hash ^= ce->getIndividualHash() * (i + 1);
329
330 jassert (hash != 0);
331 return hash;
332}
333
334AudioFile ClipEffect::getSourceFile() const
335{
336 if (auto ce = clipEffects.getClipEffect (state.getSibling (-1)))
337 return ce->getDestinationFile();
338
339 return AudioFile (clipEffects.clip.edit.engine, clipEffects.clip.getOriginalFile());
340}
341
342AudioFile ClipEffect::getDestinationFile() const
343{
344 if (destinationFile.isNull())
345 destinationFile = TemporaryFileManager::getFileForCachedFileRender (edit, getHash());
346
347 return destinationFile;
348}
349
350AudioClipBase& ClipEffect::getClip()
351{
352 return clipEffects.clip;
353}
354
355juce::UndoManager& ClipEffect::getUndoManager()
356{
357 return clipEffects.clip.edit.getUndoManager();
358}
359
360bool ClipEffect::isUsingFile (const AudioFile& af) const
361{
362 return getDestinationFile() == af;
363}
364
365void ClipEffect::valueTreeChanged()
366{
367 clipEffects.effectChanged();
368}
369
370void ClipEffect::invalidateDestination()
371{
372 destinationFile = AudioFile (edit.engine);
374}
375
376//==============================================================================
379{
381 const AudioFile& dest, const AudioFile& src,
382 SampleCount blockSizeToUse = defaultBlockSize,
383 double prerollTimeSeconds = 0)
384 : ClipEffectRenderJob (e, dest, src, blockSizeToUse),
385 node (n), prerollTime (prerollTimeSeconds)
386 {
387 jassert (node != nullptr);
388 }
389
390 bool setUpRender() override
391 {
393 callBlocking ([this] { createAndPrepareRenderContext(); });
394 return true;
395 }
396
397 bool renderNextBlock() override
398 {
400 return renderContext->render (*node, progress) == juce::ThreadPoolJob::jobHasFinished;
401 }
402
403 bool completeRender() override
404 {
405 bool ok = progress == 1.0f;
406
407 if (ok)
408 {
409 ok = destination.deleteFile();
410 (void) ok;
411 jassert (ok);
412 ok = renderContext->writer->file.getFile().moveFileTo (destination.getFile());
413 jassert (ok);
414 }
415
416 renderContext->writer->file.deleteFile();
417
418 return ok;
419 }
420
422 {
423 RenderContext (const AudioFile& destination, const AudioFile& source,
424 int numDestChannels, SampleCount blockSizeToUse, double prerollTimeS)
425 : blockSize (blockSizeToUse)
426 {
428 jassert (source.isValid());
429 jassert (numDestChannels > 0);
430 streamRange = { 0.0, source.getLength() };
431 jassert (! streamRange.isEmpty());
432
433 auto sourceInfo = source.getInfo();
434 jassert (sourceInfo.numChannels > 0 && sourceInfo.sampleRate > 0.0 && sourceInfo.bitsPerSample > 0);
435
436 AudioFile tempFile (*destination.engine,
437 destination.getFile().getSiblingFile ("temp_effect_" + juce::String::toHexString (juce::Random::getSystemRandom().nextInt64()))
438 .withFileExtension (destination.getFile().getFileExtension()));
439
440 // need to strip AIFF metadata to write to wav files
441 if (sourceInfo.metadata.getValue ("MetaDataSource", "None") == "AIFF")
442 sourceInfo.metadata.clear();
443
444 writer = std::make_unique<AudioFileWriter> (tempFile, destination.engine->getAudioFileFormatManager().getWavFormat(),
445 numDestChannels, sourceInfo.sampleRate,
446 std::max (16, sourceInfo.bitsPerSample),
447 sourceInfo.metadata, 0);
448
449 renderingBuffer = std::make_unique<juce::AudioBuffer<float>> (writer->getNumChannels(), (int) blockSize + 256);
450 auto renderingBufferChannels = juce::AudioChannelSet::canonicalChannelSet (renderingBuffer->getNumChannels());
451
452 // now prepare the render context
453 rc = std::make_unique<AudioRenderContext> (localPlayhead, streamRange,
454 renderingBuffer.get(),
455 renderingBufferChannels, 0, (int) blockSize,
456 nullptr, 0.0,
457 AudioRenderContext::playheadJumped, true);
458
459 // round pre roll timr to nearest block
460 numPreBlocks = (int) std::ceil ((prerollTimeS * sourceInfo.sampleRate) / blockSize);
461
462 auto prerollTimeRounded = numPreBlocks * blockSizeToUse / writer->getSampleRate();
463
464 streamTime = -prerollTimeRounded;
465
466 auto prerollStart = streamRange.getStart() - prerollTimeRounded;
467
468 localPlayhead.setPosition (prerollStart);
469 localPlayhead.playLockedToEngine ({ prerollStart, streamRange.getEnd() });
470 }
471
472 juce::ThreadPoolJob::JobStatus render (AudioNode& audioNode, std::atomic<float>& progressToUpdate)
473 {
475 auto blockLength = blockSize / writer->getSampleRate();
476 SampleCount samplesToWrite = juce::roundToInt ((streamRange.getEnd() - streamTime) * writer->getSampleRate());
477 auto blockEnd = std::min (streamTime + blockLength, streamRange.getEnd());
478 rc->streamTime = { streamTime, blockEnd };
479
480 // run blocks through the engine and discard
481 if (numPreBlocks-- > 0)
482 {
483 audioNode.prepareForNextBlock (*rc);
484 audioNode.renderOver (*rc);
485
486 rc->continuity = AudioRenderContext::contiguous;
487 streamTime = blockEnd;
488
490 }
491
492 auto numSamplesDone = (int) std::min (samplesToWrite, blockSize);
493 rc->bufferNumSamples = numSamplesDone;
494
495 if (numSamplesDone > 0)
496 {
497 audioNode.prepareForNextBlock (*rc);
498 audioNode.renderOver (*rc);
499 }
500
501 rc->continuity = AudioRenderContext::contiguous;
502 streamTime = blockEnd;
503
504 const float prog = (float) ((streamTime - streamRange.getStart()) / streamRange.getLength()) * 0.9f;
505 progressToUpdate = juce::jlimit (0.0f, 0.9f, prog);
506
507 // NB buffer gets trashed by this call
508 if (numSamplesDone <= 0 || ! writer->isOpen()
509 || ! writer->appendBuffer (*renderingBuffer, numSamplesDone))
510 {
511 // complete render
512 localPlayhead.stop();
513 writer->closeForWriting();
514
515 if (numSamplesDone <= 0)
516 progressToUpdate = 1.0f;
517
519 }
520
522 }
523
524 const SampleCount blockSize;
525 int numPreBlocks = 0;
527 PlayHead localPlayhead;
529
531 legacy::EditTimeRange streamRange;
532 double streamTime = 0;
533 };
534
536 std::unique_ptr<RenderContext> renderContext;
537 const double prerollTime = 0;
538
539 void createAndPrepareRenderContext()
540 {
541 {
543 node->getAudioNodeProperties (props);
544
545 renderContext = std::make_unique<RenderContext> (destination, source, props.numberOfChannels,
546 blockSize, prerollTime);
547 }
548
549 {
551 allNodes.add (node.get());
552
554 {
555 0.0,
556 renderContext->writer->getSampleRate(),
557 (int) renderContext->blockSize,
558 &allNodes,
559 renderContext->rc->playhead
560 };
561
562 node->prepareAudioNodeToPlay (info);
563 }
564 }
565
567};
568
569//==============================================================================
571{
572 BlockBasedRenderJob (Engine& e, const AudioFile& dest, const AudioFile& src, double sourceLength)
573 : ClipEffect::ClipEffectRenderJob (e, dest, src, defaultBlockSize),
574 sourceLengthSeconds (sourceLength)
575 {
576 }
577
578 bool setUpRender() override
579 {
581 auto sourceInfo = source.getInfo();
582 jassert (sourceInfo.numChannels > 0 && sourceInfo.sampleRate > 0.0 && sourceInfo.bitsPerSample > 0);
583
584 // need to strip AIFF metadata to write to wav files
585 if (sourceInfo.metadata.getValue ("MetaDataSource", "None") == "AIFF")
586 sourceInfo.metadata.clear();
587
588 reader.reset (AudioFileUtils::createReaderFor (engine, source.getFile()));
589
590 if (reader == nullptr || reader->lengthInSamples == 0)
591 return false;
592
593 sourceLengthSamples = static_cast<SampleCount> (sourceLengthSeconds * reader->sampleRate);
594
595 writer = std::make_unique<AudioFileWriter> (destination, engine.getAudioFileFormatManager().getWavFormat(),
596 sourceInfo.numChannels, sourceInfo.sampleRate,
597 std::max (16, sourceInfo.bitsPerSample),
598 sourceInfo.metadata, 0);
599
600 return writer->isOpen();
601 }
602
603 bool completeRender() override
604 {
606 reader = nullptr;
607 writer = nullptr;
608
609 return true;
610 }
611
612protected:
615
616 double sourceLengthSeconds = 0;
617 SampleCount position = 0, sourceLengthSamples = 0;
618
619 SampleCount getNumSamplesForCurrentBlock() const
620 {
621 return std::min (blockSize, sourceLengthSamples - position);
622 }
623
625};
626
627//==============================================================================
629{
630public:
631 WarpTimeEffectRenderJob (Engine& e, const AudioFile& dest, const AudioFile& src,
632 double sourceLength,
634 : BlockBasedRenderJob (e, dest, src, sourceLength),
635 clip (c), warpTimeManager (wtm)
636 {
638 auto tm = clip.getTimeStretchMode();
640 proxyInfo->clipTime = { {}, wtm.getWarpEndMarkerTime() };
641 proxyInfo->speedRatio = 1.0;
642 proxyInfo->mode = (tm != TimeStretcher::disabled && tm != TimeStretcher::melodyne)
643 ? tm : TimeStretcher::defaultMode;
644 }
645
646 bool renderNextBlock() override
647 {
649
650 // Create this here to ensure the source file is valid
651 jassert (source.isValid());
652 proxyInfo->audioSegmentList = AudioSegmentList::create (clip, warpTimeManager, source);
653
654 if (proxyInfo != nullptr
655 && writer->isOpen()
656 && proxyInfo->render (engine, source, *writer, nullptr, progress))
657 return true;
658
659 // if we fail here just copy the source to the destination to avoid loops
660 source.getFile().copyFileTo (destination.getFile());
662 return true;
663 }
664
665private:
667 AudioClipBase& clip;
668 WarpTimeManager& warpTimeManager;
669
671};
672
673//==============================================================================
674VolumeEffect::VolumeEffect (const juce::ValueTree& v, ClipEffects& o)
675 : ClipEffect (v, o)
676{
677 auto volState = state.getChildWithName (IDs::PLUGIN);
678
679 if (! volState.isValid())
680 {
681 volState = VolumeAndPanPlugin::create();
682 volState.setProperty (IDs::volume, decibelsToVolumeFaderPosition (0.0f), nullptr);
683 state.addChild (volState, -1, nullptr);
684 }
685
686 plugin = new VolumeAndPanPlugin (edit, volState, false);
687}
688
690 double sourceLength)
691{
693 legacy::EditTimeRange timeRange (0.0, sourceLength);
694 jassert (! timeRange.isEmpty());
695
696 auto n = new WaveAudioNode (sourceFile, timeRange, 0.0, {}, {},
698
699 return new AudioNodeRenderJob (edit.engine, new PluginAudioNode (plugin, n, false),
700 getDestinationFile(), sourceFile, 128);
701}
702
704{
705 return true;
706}
707
708void VolumeEffect::propertiesButtonPressed (SelectionManager& sm)
709{
710 if (plugin != nullptr)
711 sm.selectOnly (*plugin);
712}
713
714HashCode VolumeEffect::getIndividualHash() const
715{
716 return plugin != nullptr ? hashPlugin (state, *plugin) : 0;
717}
718
719void VolumeEffect::valueTreePropertyChanged (juce::ValueTree& v, const juce::Identifier& i)
720{
721 // This is the automation writing back the AttachedValue so we need to ignore it
722 if (plugin == nullptr || (plugin->isAutomationNeeded()
723 && v == plugin->state
724 && (matchesAnyOf (i, { IDs::volume, IDs::pan }))))
725 return;
726
728}
729
730void VolumeEffect::valueTreeChanged()
731{
732 ClipEffect::valueTreeChanged();
733
734 inhibitor = std::make_unique<ClipEffects::RenderInhibitor> (clipEffects);
735 startTimer (500);
736}
737
738void VolumeEffect::timerCallback()
739{
740 if (juce::Component::isMouseButtonDownAnywhere())
741 return;
742
743 inhibitor = nullptr;
744 stopTimer();
745}
746
747//==============================================================================
748FadeInOutEffect::FadeInOutEffect (const juce::ValueTree& v, ClipEffects& o)
749 : ClipEffect (v, o)
750{
751 auto um = &getUndoManager();
752 fadeIn.referTo (state, IDs::fadeIn, um);
753 fadeOut.referTo (state, IDs::fadeOut, um);
754
755 fadeInType.referTo (state, IDs::fadeInType, um, AudioFadeCurve::linear);
756 fadeOutType.referTo (state, IDs::fadeOutType, um, AudioFadeCurve::linear);
757}
758
759void FadeInOutEffect::setFadeIn (TimeDuration in)
760{
761 const auto l = clipEffects.getEffectsLength() * clipEffects.getSpeedRatioEstimate();
762 in = juce::jlimit (TimeDuration(), l, in);
763
764 if (in + fadeOut > l)
765 {
766 const double scale = l / (in + fadeOut);
767 fadeIn = in * scale;
768 fadeOut = fadeOut * scale;
769 }
770 else
771 {
772 fadeIn = in;
773 }
774}
775
776void FadeInOutEffect::setFadeOut (TimeDuration out)
777{
778 const auto l = clipEffects.getEffectsLength() * clipEffects.getSpeedRatioEstimate();
779 out = juce::jlimit (TimeDuration(), l, out);
780
781 if (fadeIn + out > l)
782 {
783 const double scale = l / (fadeIn + out);
784 fadeIn = fadeIn * scale;
785 fadeOut = out * scale;
786 }
787 else
788 {
789 fadeOut = out;
790 }
791}
792
794 double sourceLength)
795{
797 AudioFile destFile (getDestinationFile());
798 legacy::EditTimeRange timeRange (0.0, sourceLength);
799 jassert (! timeRange.isEmpty());
800
801 AudioNode* n = new WaveAudioNode (sourceFile, timeRange, 0.0, {}, {},
803 auto blockSize = AudioNodeRenderJob::defaultBlockSize;
804
805 auto speedRatio = clipEffects.getSpeedRatioEstimate();
806 auto effectRange = clipEffects.getEffectsRange();
807 effectRange = { effectRange.getStart() * speedRatio,
808 effectRange.getEnd() * speedRatio };
809
810 const TimeRange fadeInRange (effectRange.getStart(), effectRange.getStart() + fadeIn);
811 const TimeRange fadeOutRange (effectRange.getEnd() - fadeOut, effectRange.getEnd());
812
813 switch (getType())
814 {
815 case EffectType::fadeInOut:
816 if (fadeIn.get() > TimeDuration() || fadeOut.get() > TimeDuration())
817 n = new FadeInOutAudioNode (n,
818 toEditTimeRange (fadeInRange), toEditTimeRange (fadeOutRange),
819 fadeInType, fadeOutType);
820
821 break;
822
823 case EffectType::tapeStartStop:
824 if (fadeIn.get() > TimeDuration() || fadeOut.get() > TimeDuration())
825 {
826 n = new SpeedRampAudioNode (n,
827 toEditTimeRange (fadeInRange), toEditTimeRange (fadeOutRange),
828 fadeInType, fadeOutType);
829 blockSize = 128;
830 }
831
832 break;
833
834 case EffectType::none:
835 case EffectType::volume:
836 case EffectType::stepVolume:
837 case EffectType::pitchShift:
838 case EffectType::warpTime:
839 case EffectType::normalise:
840 case EffectType::makeMono:
841 case EffectType::reverse:
842 case EffectType::invert:
843 case EffectType::filter:
844 default:
846 break;
847 }
848
849 n = new TimedMutingAudioNode (n, { legacy::EditTimeRange ({}, effectRange.getStart().inSeconds()),
850 legacy::EditTimeRange (effectRange.getEnd().inSeconds(), sourceLength) });
851
852 return new AudioNodeRenderJob (edit.engine, n, destFile, sourceFile, blockSize);
853}
854
855HashCode FadeInOutEffect::getIndividualHash() const
856{
857 auto effectRange = clipEffects.getEffectsRange();
858
859 return ClipEffect::getIndividualHash()
860 ^ static_cast<HashCode> (clipEffects.getSpeedRatioEstimate() * 6345.2)
861 ^ static_cast<HashCode> (effectRange.getStart().inSeconds() * 3526.9)
862 ^ static_cast<HashCode> (effectRange.getEnd().inSeconds() * 53625.3);
863}
864
865//==============================================================================
866StepVolumeEffect::StepVolumeEffect (const juce::ValueTree& v, ClipEffects& o)
867 : ClipEffect (v, o)
868{
870 const bool newEffect = ! (state.hasProperty (IDs::noteLength) && state.hasProperty (IDs::crossfadeLength));
871
872 auto um = &getUndoManager();
873 noteLength.referTo (state, IDs::noteLength, um, BeatDuration::fromBeats (0.25));
874 crossfade.referTo (state, IDs::crossfadeLength, um, 0.01);
875 pattern.referTo (state, IDs::pattern, um);
876
877 if (newEffect && ! edit.isLoading())
878 {
879 state.setProperty (IDs::noteLength, noteLength.get().inBeats(), um);
880 state.setProperty (IDs::crossfadeLength, crossfade.get(), um);
881
882 Pattern (*this).toggleAtInterval (2);
883 }
884}
885
886StepVolumeEffect::~StepVolumeEffect()
887{
888 notifyListenersOfDeletion();
889}
890
891int StepVolumeEffect::getMaxNumNotes()
892{
893 auto& ts = edit.tempoSequence;
894 auto& c = getClip();
895 auto pos = c.getPosition();
896
897 auto startTime = pos.getStartOfSource();
898 auto startBeat = ts.toBeats (startTime);
899 auto endBeat = ts.toBeats (startTime + (c.getSourceLength() / clipEffects.getSpeedRatioEstimate()));
900
901 return (int) std::ceil ((endBeat - startBeat) / noteLength);
902}
903
905 double sourceLength)
906{
908 jassert (sourceLength > 0);
909
910 auto destFile = getDestinationFile();
911 legacy::EditTimeRange timeRange (0.0, sourceLength);
912
913 auto speedRatio = clipEffects.getSpeedRatioEstimate();
914 auto effectRange = clipEffects.getEffectsRange();
915
916 auto fade = crossfade.get();
917 auto halfCrossfade = TimeDuration::fromSeconds (fade / 2.0);
918 juce::Array<TimeRange> nonMuteTimes;
919
920 // Calculate non-mute times
921 {
922 const StepVolumeEffect::Pattern p (*this);
923 auto cache = p.getPattern();
924 auto& ts = edit.tempoSequence;
925 auto& c = getClip();
926 auto pos = c.getPosition();
927
928 auto length = noteLength.get();
929 auto startTime = pos.getStart();
930
931 auto startBeat = ts.toBeats (pos.getStart() + toDuration (effectRange.getStart()));
932 auto endBeat = ts.toBeats (pos.getEnd());
933 auto numNotes = std::min (p.getNumNotes(), (int) std::ceil ((endBeat - startBeat) / length));
934
935 auto beat = startBeat;
936
937 for (int i = 0; i <= numNotes; ++i)
938 {
939 if (! cache[i])
940 {
941 beat = beat + length;
942 continue;
943 }
944
945 auto s = ts.toTime (beat) - toDuration (startTime);
946 beat = beat + length;
947 auto e = ts.toTime (beat) - toDuration (startTime);
948
949 nonMuteTimes.add ({ s - halfCrossfade,
950 e + halfCrossfade });
951 }
952
953 // Strip adjacent times
954 auto lastTime = nonMuteTimes.getLast();
955
956 for (int i = nonMuteTimes.size() - 1; --i >= 0;)
957 {
958 auto& thisTime = nonMuteTimes.getReference (i);
959
960 if (thisTime.getEnd() >= lastTime.getStart())
961 {
962 thisTime = thisTime.withEnd (lastTime.getEnd());
963 nonMuteTimes.remove (i + 1);
964 }
965
966 lastTime = thisTime;
967 }
968
969 // Scale everything by the speed ratio
970 for (auto& t : nonMuteTimes)
971 t = t.rescaled (TimePosition(), speedRatio);
972 }
973
974 auto waveNode = new WaveAudioNode (sourceFile, timeRange, 0.0, {}, {},
976 auto compNode = createTrackCompAudioNode (waveNode,
977 TrackCompManager::TrackComp::getMuteTimes (nonMuteTimes),
978 nonMuteTimes, TimeDuration::fromSeconds (fade));
979
980 return new AudioNodeRenderJob (edit.engine, compNode, destFile, sourceFile);
981}
982
984{
985 return true;
986}
987
988void StepVolumeEffect::propertiesButtonPressed (SelectionManager& sm)
989{
990 sm.selectOnly (this);
991}
992
993//==============================================================================
995{
996 return TRANS("Step Volume Effect Editor");
997}
998
999HashCode StepVolumeEffect::getIndividualHash() const
1000{
1001 auto effectRange = clipEffects.getEffectsRange();
1002
1003 return ClipEffect::getIndividualHash()
1004 ^ static_cast<HashCode> (clipEffects.getSpeedRatioEstimate() * 6345.2)
1005 ^ static_cast<HashCode> (effectRange.getStart().inSeconds() * 3526.9)
1006 ^ static_cast<HashCode> (effectRange.getEnd().inSeconds() * 53625.3);
1007}
1008
1009//==============================================================================
1010StepVolumeEffect::Pattern::Pattern (StepVolumeEffect& o) : effect (o), state (o.state)
1011{
1012}
1013
1014StepVolumeEffect::Pattern::Pattern (const Pattern& other) noexcept
1015 : effect (other.effect), state (other.state)
1016{
1017}
1018
1019bool StepVolumeEffect::Pattern::getNote (int index) const noexcept
1020{
1021 return getPattern()[index];
1022}
1023
1024void StepVolumeEffect::Pattern::setNote (int index, bool value)
1025{
1026 if (getNote (index) != value)
1027 {
1028 auto p = getPattern();
1029 p.setBit (index, value);
1030 setPattern (p);
1031 }
1032}
1033
1034juce::BigInteger StepVolumeEffect::Pattern::getPattern() const noexcept
1035{
1037 b.parseString (state[IDs::pattern].toString(), 2);
1038 return b;
1039}
1040
1041void StepVolumeEffect::Pattern::setPattern (const juce::BigInteger& b) noexcept
1042{
1043 state.setProperty (IDs::pattern, b.toString (2), &effect.getUndoManager());
1044}
1045
1046void StepVolumeEffect::Pattern::clear()
1047{
1048 setPattern ({});
1049}
1050
1051int StepVolumeEffect::Pattern::getNumNotes() const
1052{
1053 return getPattern().getHighestBit();
1054}
1055
1056void StepVolumeEffect::Pattern::shiftChannel (bool toTheRight)
1057{
1058 auto c = getPattern();
1059
1060 // NB: Notes are added in reverse order
1061 if (toTheRight)
1062 c <<= 1;
1063 else
1064 c >>= 1;
1065
1066 setPattern (c);
1067}
1068
1069void StepVolumeEffect::Pattern::toggleAtInterval (int interval)
1070{
1071 auto c = getPattern();
1072
1073 for (int i = effect.getMaxNumNotes(); --i >= 0;)
1074 c.setBit (i, (i % interval) == 0);
1075
1076 setPattern (c);
1077}
1078
1079void StepVolumeEffect::Pattern::randomiseChannel()
1080{
1081 clear();
1082 juce::Random r;
1083
1084 for (int i = 0; i < effect.getMaxNumNotes(); ++i)
1085 setNote (i, r.nextBool());
1086}
1087
1088//==============================================================================
1089PitchShiftEffect::PitchShiftEffect (const juce::ValueTree& v, ClipEffects& o)
1090 : ClipEffect (v, o)
1091{
1093 auto pitchState = state.getChildWithName (IDs::PLUGIN);
1094
1095 if (! pitchState.isValid())
1096 {
1097 pitchState = PitchShiftPlugin::create();
1098 state.addChild (pitchState, -1, nullptr);
1099 }
1100
1101 plugin = new PitchShiftPlugin (edit, pitchState);
1102}
1103
1104void PitchShiftEffect::initialise()
1105{
1106 if (plugin != nullptr)
1107 for (auto ap : plugin->getAutomatableParameters())
1108 ap->updateStream();
1109}
1110
1112{
1114 const legacy::EditTimeRange timeRange (0.0, sourceLength);
1115 jassert (! timeRange.isEmpty());
1116
1117 auto n = new WaveAudioNode (sourceFile, timeRange, 0.0, {}, {},
1119
1120 // Use 1.0 second of preroll to be safe. We can't ask the plugin since it
1121 // may not be initialized yet
1122 return new AudioNodeRenderJob (edit.engine, new PluginAudioNode (plugin, n, false),
1123 getDestinationFile(), sourceFile, 512, 1.0);
1124}
1125
1127{
1128 return true;
1129}
1130
1131void PitchShiftEffect::propertiesButtonPressed (SelectionManager& sm)
1132{
1133 if (plugin != nullptr)
1134 sm.selectOnly (*plugin);
1135}
1136
1137HashCode PitchShiftEffect::getIndividualHash() const
1138{
1139 return hashPlugin (state, *plugin);
1140}
1141
1142void PitchShiftEffect::valueTreePropertyChanged (juce::ValueTree& v, const juce::Identifier& i)
1143{
1144 // This is the automation writing back the AttachedValue so we need to ignore it
1145 if (plugin != nullptr
1146 && v == plugin->state
1147 && matchesAnyOf (i, { IDs::semitonesUp })
1148 && plugin->isAutomationNeeded())
1149 return;
1150
1152}
1153
1154void PitchShiftEffect::valueTreeChanged()
1155{
1156 ClipEffect::valueTreeChanged();
1157
1158 inhibitor = std::make_unique<ClipEffects::RenderInhibitor> (clipEffects);
1159 startTimer (250);
1160}
1161
1162void PitchShiftEffect::timerCallback()
1163{
1164 if (juce::Component::isMouseButtonDownAnywhere())
1165 return;
1166
1167 inhibitor = nullptr;
1168 stopTimer();
1169}
1170
1171//==============================================================================
1172WarpTimeEffect::WarpTimeEffect (const juce::ValueTree& v, ClipEffects& o)
1173 : ClipEffect (v, o)
1174{
1176 warpTimeManager = new WarpTimeManager (edit, AudioFile (edit.engine), state);
1177 editLoadedCallback = std::make_unique<Edit::LoadFinishedCallback<WarpTimeEffect>> (*this, edit);
1178}
1179
1181 double sourceLength)
1182{
1184 return new WarpTimeEffectRenderJob (edit.engine, getDestinationFile(), sourceFile,
1185 sourceLength, *warpTimeManager, getClip());
1186}
1187
1188HashCode WarpTimeEffect::getIndividualHash() const
1189{
1190 return warpTimeManager->getHash();
1191}
1192
1194{
1195 warpTimeManager->setSourceFile (getSourceFile());
1196}
1197
1198void WarpTimeEffect::editFinishedLoading()
1199{
1200 sourceChanged();
1201 editLoadedCallback = nullptr;
1202}
1203
1204//==============================================================================
1206{
1208 : plugin (p), callback (std::move (cb))
1209 {
1210 if (plugin->isProcessingEnabled())
1211 callback();
1212 }
1213
1214 ~PluginUnloadInhibitor() override
1215 {
1216 if (count > 0)
1217 unload();
1218 }
1219
1220 void increase()
1221 {
1222 if (count++ == 0)
1223 load();
1224 }
1225
1226 void increaseForJob (int ms, juce::ReferenceCountedObjectPtr<AudioNodeRenderJob> job)
1227 {
1228 if (! isTimerRunning())
1229 increase();
1230
1231 jobs.add (job);
1232 startTimer (ms);
1233 }
1234
1235 void decrease()
1236 {
1237 if (--count == 0)
1238 unload();
1239 }
1240
1241 void timerCallback() override
1242 {
1243 for (int i = jobs.size(); --i >= 0;)
1244 {
1245 if (jobs[i]->progress >= 1.0f)
1246 jobs.remove (i);
1247 else
1248 return;
1249 }
1250
1251 decrease();
1252 stopTimer();
1253 }
1254
1255 void load()
1256 {
1257 callBlocking ([this]() { plugin->setProcessingEnabled (true); callback(); });
1258 }
1259
1260 void unload()
1261 {
1262 callBlocking ([this]() { plugin->setProcessingEnabled (false); callback(); });
1263 }
1264
1265 int count = 0;
1266 Plugin::Ptr plugin;
1268 std::function<void(void)> callback;
1269
1271};
1272
1273//==============================================================================
1274ScopedPluginUnloadInhibitor::ScopedPluginUnloadInhibitor (PluginUnloadInhibitor& o) : owner (o) { owner.increase(); }
1275ScopedPluginUnloadInhibitor::~ScopedPluginUnloadInhibitor() { owner.decrease(); }
1276
1277//==============================================================================
1278PluginEffect::PluginEffect (const juce::ValueTree& v, ClipEffects& o)
1279 : ClipEffect (v, o)
1280{
1282 auto um = &getUndoManager();
1283 currentCurve.referTo (state, IDs::currentCurve, um);
1284 lastHash.referTo (state, IDs::hash, nullptr);
1285
1286 auto pluginState = state.getChildWithName (IDs::PLUGIN);
1287 jassert (pluginState.isValid());
1288
1289 if (pluginState.isValid())
1290 {
1291 pluginState.setProperty (IDs::process, false, nullptr); // always restore plugin to non-processing state on load
1292 callBlocking ([this, pluginState]() { plugin = edit.getPluginCache().getOrCreatePluginFor (pluginState); });
1293 }
1294
1295 if (plugin != nullptr)
1296 {
1297 auto loadCallback = [this]()
1298 {
1299 if (plugin != nullptr)
1300 {
1301 if (plugin->isProcessingEnabled())
1302 {
1303 for (int i = 0; i < plugin->getNumAutomatableParameters(); i++)
1304 plugin->getAutomatableParameter (i)->addListener (this);
1305 }
1306 else
1307 {
1308 for (int i = 0; i < plugin->getNumAutomatableParameters(); i++)
1309 plugin->getAutomatableParameter (i)->removeListener (this);
1310 }
1311 }
1312 };
1313
1314 pluginUnloadInhibitor = std::make_unique<PluginUnloadInhibitor> (plugin, loadCallback);
1315 }
1316}
1317
1319 double sourceLength)
1320{
1322
1323 const ScopedPluginUnloadInhibitor lock (*pluginUnloadInhibitor);
1324
1325 const legacy::EditTimeRange timeRange (0.0, sourceLength);
1326 jassert (! timeRange.isEmpty());
1327
1328 AudioNode* n = new WaveAudioNode (sourceFile, timeRange, 0.0, {}, {},
1330
1331 if (plugin != nullptr)
1332 {
1333 plugin->setProcessingEnabled (true);
1334 n = new PluginAudioNode (plugin, n, false);
1335 }
1336
1337 // Use 1.0 second of preroll to be safe. We can't ask the plugin since it
1338 // may not be initialized yet
1339 auto job = new AudioNodeRenderJob (edit.engine, n, getDestinationFile(), sourceFile, 512, 1.0);
1340
1341 if (pluginUnloadInhibitor != nullptr)
1342 pluginUnloadInhibitor->increaseForJob (30 * 1000, job);
1343
1344 return job;
1345}
1346
1347void PluginEffect::flushStateToValueTree()
1348{
1349 if (plugin != nullptr)
1350 plugin->flushPluginStateToValueTree();
1351}
1352
1354{
1355 return true;
1356}
1357
1358void PluginEffect::propertiesButtonPressed (SelectionManager& sm)
1359{
1360 if (plugin != nullptr)
1361 {
1362 sm.selectOnly (*plugin);
1363
1364 plugin->showWindowExplicitly();
1365 }
1366}
1367
1368HashCode PluginEffect::getIndividualHash() const
1369{
1370 jassert (plugin != nullptr);
1371
1372 // only calculate a hash if the plugin is loaded. If the plugin is unloaded,
1373 // the hash can't change
1374 if (plugin != nullptr && (plugin->isProcessingEnabled() || lastHash == 0))
1375 {
1376 const ScopedPluginUnloadInhibitor lock (*pluginUnloadInhibitor);
1377 lastHash = (juce::int64) hashPlugin (state, *plugin);
1378 }
1379
1380 return lastHash;
1381}
1382
1383void PluginEffect::valueTreePropertyChanged (juce::ValueTree& v, const juce::Identifier& i)
1384{
1385 // This is the automation writing back the AttachedValue so we need to ignore it
1386 if (plugin != nullptr
1387 && v == plugin->state
1388 && plugin->isAutomationNeeded())
1389 return;
1390
1391 if (matchesAnyOf (i, { IDs::hash, IDs::process }))
1392 return;
1393
1395}
1396
1397void PluginEffect::valueTreeChanged()
1398{
1399 // Override this to avoid the plugin's properties constantly triggering re-renders
1400}
1401
1402void PluginEffect::timerCallback()
1403{
1404 if (juce::Component::isMouseButtonDownAnywhere())
1405 return;
1406
1407 inhibitor = nullptr;
1408 stopTimer();
1409}
1410
1412{
1413 ClipEffect::valueTreeChanged();
1414
1415 if (inhibitor == nullptr)
1416 inhibitor = std::make_unique<ClipEffects::RenderInhibitor> (clipEffects);
1417
1418 startTimer (250);
1419}
1420
1421//==============================================================================
1423{
1424 NormaliseRenderJob (Engine& e, const AudioFile& dest, const AudioFile& src, double sourceLength, double gain)
1425 : BlockBasedRenderJob (e, dest, src, sourceLength), maxGain (gain) {}
1426
1427 bool renderNextBlock() override
1428 {
1430
1431 if (! calculatedGain)
1432 {
1433 calculatedGain = true;
1434
1435 float lmin, lmax, rmin, rmax;
1436 reader->readMaxLevels (0, sourceLengthSamples, lmin, lmax, rmin, rmax);
1437
1438 auto maxLevel = juce::jmax (-lmin, lmax, -rmin, rmax);
1439 gainFactor = dbToGain (float (maxGain)) / maxLevel;
1440 }
1441
1442 auto todo = (int) getNumSamplesForCurrentBlock();
1443
1444 AudioScratchBuffer scratch ((int) reader->numChannels, todo);
1445
1446 reader->read (&scratch.buffer, 0, todo, position, true, true);
1447
1448 scratch.buffer.applyGain (0, todo, gainFactor);
1449
1450 writer->appendBuffer (scratch.buffer, todo);
1451
1452 position += todo;
1453 progress = float (position) / float (sourceLengthSamples);
1454
1455 return position >= sourceLengthSamples;
1456 }
1457
1458 const double maxGain = 1.0;
1459
1460 bool calculatedGain = false;
1461 float gainFactor = 0.0f;
1462};
1463
1464NormaliseEffect::NormaliseEffect (const juce::ValueTree& v, ClipEffects& o)
1465 : ClipEffect (v, o)
1466{
1467 auto um = &getUndoManager();
1468 maxLevelDB.referTo (state, IDs::level, um, 0.0);
1469}
1470
1471NormaliseEffect::~NormaliseEffect()
1472{
1473 notifyListenersOfDeletion();
1474}
1475
1477{
1479
1480 return new NormaliseRenderJob (edit.engine, getDestinationFile(),
1481 sourceFile, sourceLength, maxLevelDB.get());
1482}
1483
1485{
1486 return true;
1487}
1488
1489void NormaliseEffect::propertiesButtonPressed (SelectionManager& sm)
1490{
1491 sm.selectOnly (this);
1492}
1493
1495{
1496 return TRANS("Normalise Effect Editor");
1497}
1498
1499//==============================================================================
1501{
1502 MakeMonoRenderJob (Engine& e, const AudioFile& dest, const AudioFile& src, double sourceLength, SrcChannels srcCh)
1503 : BlockBasedRenderJob (e, dest, src, sourceLength), srcChannels (srcCh) {}
1504
1505 bool setUpRender() override
1506 {
1508 auto sourceInfo = source.getInfo();
1509 jassert (sourceInfo.numChannels > 0 && sourceInfo.sampleRate > 0.0 && sourceInfo.bitsPerSample > 0);
1510
1511 // need to strip AIFF metadata to write to wav files
1512 if (sourceInfo.metadata.getValue ("MetaDataSource", "None") == "AIFF")
1513 sourceInfo.metadata.clear();
1514
1515 reader.reset (AudioFileUtils::createReaderFor (engine, source.getFile()));
1516
1517 if (reader == nullptr || reader->lengthInSamples == 0)
1518 return false;
1519
1520 sourceLengthSamples = static_cast<SampleCount> (sourceLengthSeconds * reader->sampleRate);
1521
1522 writer = std::make_unique<AudioFileWriter> (destination, engine.getAudioFileFormatManager().getWavFormat(),
1523 1, sourceInfo.sampleRate,
1524 std::max (16, sourceInfo.bitsPerSample),
1525 sourceInfo.metadata, 0);
1526
1527 return writer->isOpen();
1528 }
1529
1530 bool renderNextBlock() override
1531 {
1533 auto todo = (int) getNumSamplesForCurrentBlock();
1534
1535 AudioScratchBuffer input ((int) reader->numChannels, todo);
1536 reader->read (&input.buffer, 0, todo, position, true, true);
1537
1538 if (reader->numChannels == 1)
1539 {
1540 writer->appendBuffer (input.buffer, todo);
1541 }
1542 else
1543 {
1544 AudioScratchBuffer output (1, todo);
1545
1546 if (srcChannels == chLR)
1547 {
1548 output.buffer.copyFrom (0, 0, input.buffer.getReadPointer (0), todo, 0.5f);
1549 output.buffer.addFrom (0, 0, input.buffer.getReadPointer (1), todo, 0.5f);
1550 }
1551 else if (srcChannels == chL)
1552 {
1553 output.buffer.copyFrom (0, 0, input.buffer.getReadPointer (0), todo);
1554 }
1555 else if (srcChannels == chR)
1556 {
1557 output.buffer.copyFrom (0, 0, input.buffer.getReadPointer (1), todo);
1558 }
1559 else
1560 {
1562 }
1563
1564 writer->appendBuffer (output.buffer, todo);
1565 }
1566
1567 position += todo;
1568 progress = float(position) / float(sourceLengthSamples);
1569
1570 return position >= sourceLengthSamples;
1571 }
1572
1573 const SrcChannels srcChannels;
1574};
1575
1576MakeMonoEffect::MakeMonoEffect (const juce::ValueTree& v, ClipEffects& o)
1577 : ClipEffect (v, o)
1578{
1579 auto um = &getUndoManager();
1580 channels.referTo (state, IDs::channels, um, 0);
1581}
1582
1583MakeMonoEffect::~MakeMonoEffect()
1584{
1585 notifyListenersOfDeletion();
1586}
1587
1589{
1591 return new MakeMonoRenderJob (edit.engine, getDestinationFile(), sourceFile,
1592 sourceLength, (SrcChannels) channels.get());
1593}
1594
1596{
1597 return true;
1598}
1599
1600void MakeMonoEffect::propertiesButtonPressed (SelectionManager& sm)
1601{
1602 sm.selectOnly (this);
1603}
1604
1606{
1607 return TRANS("Make Mono Editor");
1608}
1609
1610//==============================================================================
1612{
1613 ReverseRenderJob (Engine& e, const AudioFile& dest, const AudioFile& src, double sourceLength)
1614 : BlockBasedRenderJob (e, dest, src, sourceLength) {}
1615
1616 bool renderNextBlock() override
1617 {
1619 auto todo = (int) getNumSamplesForCurrentBlock();
1620
1621 AudioScratchBuffer scratch ((int) reader->numChannels, todo);
1622
1623 reader->read (&scratch.buffer, 0, todo, sourceLengthSamples - position - todo, true, true);
1624
1625 scratch.buffer.reverse (0, todo);
1626
1627 writer->appendBuffer (scratch.buffer, todo);
1628
1629 position += todo;
1630 progress = float(position) / float(sourceLengthSamples);
1631
1632 return position >= sourceLengthSamples;
1633 }
1634};
1635
1636ReverseEffect::ReverseEffect (const juce::ValueTree& v, ClipEffects& o)
1637 : ClipEffect (v, o)
1638{
1639}
1640
1642 double sourceLength)
1643{
1645 return new ReverseRenderJob (edit.engine, getDestinationFile(), sourceFile, sourceLength);
1646}
1647
1648//==============================================================================
1650{
1651 InvertRenderJob (Engine& e, const AudioFile& dest, const AudioFile& src, double sourceLength)
1652 : BlockBasedRenderJob (e, dest, src, sourceLength) {}
1653
1654 bool renderNextBlock() override
1655 {
1657 auto todo = (int) getNumSamplesForCurrentBlock();
1658
1659 AudioScratchBuffer scratch ((int) reader->numChannels, todo);
1660
1661 reader->read (&scratch.buffer, 0, todo, position, true, true);
1662
1663 scratch.buffer.applyGain (0, todo, -1.0f);
1664
1665 writer->appendBuffer (scratch.buffer, todo);
1666
1667 position += todo;
1668 progress = float (position) / float (sourceLengthSamples);
1669
1670 return position >= sourceLengthSamples;
1671 }
1672};
1673
1674InvertEffect::InvertEffect (const juce::ValueTree& v, ClipEffects& o)
1675 : ClipEffect (v, o)
1676{
1677}
1678
1680 double sourceLength)
1681{
1683 return new InvertRenderJob (edit.engine, getDestinationFile(), sourceFile, sourceLength);
1684}
1685
1686//==============================================================================
1687ClipEffect* ClipEffect::create (const juce::ValueTree& v, ClipEffects& ce)
1688{
1690 {
1691 case EffectType::none: jassertfalse; return {};
1692 case EffectType::volume: return new VolumeEffect (v, ce);
1693 case EffectType::fadeInOut:
1694 case EffectType::tapeStartStop: return new FadeInOutEffect (v, ce);
1695 case EffectType::stepVolume: return new StepVolumeEffect (v, ce);
1696 case EffectType::pitchShift: return new PitchShiftEffect (v, ce);
1697 case EffectType::warpTime: return new WarpTimeEffect (v, ce);
1698 case EffectType::normalise: return new NormaliseEffect (v, ce);
1699 case EffectType::makeMono: return new MakeMonoEffect (v, ce);
1700 case EffectType::reverse: return new ReverseEffect (v, ce);
1701 case EffectType::invert: return new InvertEffect (v, ce);
1702 case EffectType::filter: return new PluginEffect (v, ce);
1703 default: jassertfalse; return {};
1704 }
1705}
1706
1707//==============================================================================
1709{
1710 AggregateJob (Engine& e, const AudioFile& destFile, const AudioFile& source,
1712 : Job (e, destFile),
1713 sourceFile (source), lastFile (source.getFile()),
1714 jobs (std::move (j)), originalNumTasks (jobs.size())
1715 {
1716 }
1717
1718 bool setUpRender() override
1719 {
1720 return true;
1721 }
1722
1723 bool renderNextBlock() override
1724 {
1726 if (! sourceFile.isValid())
1727 {
1728 juce::Thread::sleep (100);
1729 return false;
1730 }
1731
1732 if (currentJob == nullptr)
1733 {
1734 currentJob = jobs.removeAndReturn (0);
1735
1736 if (currentJob != nullptr)
1737 if (! currentJob->setUpRender())
1738 return true;
1739 }
1740 else
1741 {
1742 if (currentJob->renderNextBlock())
1743 {
1744 if (! currentJob->completeRender())
1745 return true;
1746
1747 auto& afm = engine.getAudioFileManager();
1748 afm.releaseFile (currentJob->destination);
1749
1750 if (! currentJob->destination.isNull())
1751 callBlocking ([&afm, fileToValidate = currentJob->destination]
1752 {
1753 afm.validateFile (fileToValidate, true);
1754 jassert (fileToValidate.isValid());
1755 });
1756
1757 lastFile = currentJob->destination.getFile();
1758 currentJob = nullptr;
1759 ++numJobsCompleted;
1760 }
1761 }
1762
1763 const float jobShare = 1.0f / std::max (1, originalNumTasks);
1764 progress = (numJobsCompleted * jobShare) + (jobShare * (currentJob != nullptr ? currentJob->progress.load() : 0.0f));
1765
1766 return currentJob == nullptr && jobs.isEmpty();
1767 }
1768
1769 bool completeRender() override
1770 {
1771 return lastFile.copyFileTo (proxy.getFile()) && jobs.isEmpty();
1772 }
1773
1774 const AudioFile sourceFile;
1775 juce::File lastFile;
1778 const int originalNumTasks;
1779 int numJobsCompleted = 0;
1780
1782};
1783
1784//==============================================================================
1785RenderManager::Job::Ptr ClipEffects::createRenderJob (const AudioFile& destFile, const AudioFile& sourceFile) const
1786{
1788 clip.edit.getTransport().forceOrphanFreezeAndProxyFilesPurge();
1789
1790 const double length = sourceFile.getLength();
1791 AudioFile inputFile (sourceFile);
1793
1794 for (auto ce : objects)
1795 {
1796 const AudioFile af (ce->getDestinationFile());
1797
1798 if (af.getFile().existsAsFile() && af.isValid())
1799 {
1800 inputFile = af;
1801 }
1802 else if (ClipEffect::ClipEffectRenderJob::Ptr j = ce->createRenderJob (inputFile, length))
1803 {
1804 inputFile = j->destination;
1805 jobs.add (j);
1806 }
1807 }
1808
1809 AudioFile firstFile (jobs.isEmpty() ? inputFile : jobs.getFirst()->source);
1810
1811 return new AggregateJob (clip.edit.engine, destFile, firstFile, std::move (jobs));
1812}
1813
1814}} // namespace tracktion { inline namespace engine
T ceil(T... args)
int size() const noexcept
void remove(int indexToRemove)
void add(const ElementType &newElement)
ElementType & getReference(int index) noexcept
ElementType getLast() const noexcept
void copyFrom(int destChannel, int destStartSample, const AudioBuffer &source, int sourceChannel, int sourceStartSample, int numSamples) noexcept
void reverse(int channel, int startSample, int numSamples) const noexcept
void addFrom(int destChannel, int destStartSample, const AudioBuffer &source, int sourceChannel, int sourceStartSample, int numSamples, Type gainToApplyToSource=Type(1)) noexcept
const Type * getReadPointer(int channelNumber) const noexcept
void applyGain(int channel, int startSample, int numSamples, Type gain) noexcept
static AudioChannelSet JUCE_CALLTYPE stereo()
static AudioChannelSet JUCE_CALLTYPE canonicalChannelSet(int numChannels)
Type get() const noexcept
String getFileExtension() const
File getSiblingFile(StringRef siblingFileName) const
File withFileExtension(StringRef newExtension) const
bool isValid() const noexcept
const String & toString() const noexcept
bool nextBool() noexcept
int64 nextInt64() noexcept
static Random & getSystemRandom() noexcept
bool isEmpty() const noexcept
ObjectClass * add(ObjectClass *newObject)
int64 hashCode64() const noexcept
static String toHexString(IntegerType number)
void stopTimer() noexcept
bool isTimerRunning() const noexcept
void startTimer(int intervalInMilliseconds) noexcept
virtual void valueTreePropertyChanged(ValueTree &treeWhosePropertyHasChanged, const Identifier &property)
void removeChild(const ValueTree &child, UndoManager *undoManager)
ValueTree getChild(int index) const
int getNumChildren() const noexcept
bool isValid() const noexcept
ValueTree & setProperty(const Identifier &name, const var &newValue, UndoManager *undoManager)
void addListener(Listener *listener)
int indexOf(const ValueTree &child) const noexcept
void addChild(const ValueTree &child, int index, UndoManager *undoManager)
ValueTree getParent() const noexcept
ValueTree getChildWithName(const Identifier &type) const
void removeProperty(const Identifier &name, UndoManager *undoManager)
ValueTree getSibling(int delta) const noexcept
bool hasProperty(const Identifier &name) const noexcept
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.
Base class for nodes in an audio playback graph.
An audio scratch buffer that has pooled storage.
juce::AudioBuffer< float > & buffer
The buffer to use.
HashCode getHash() const
Returns the hash for this effect.
virtual void sourceChanged()
Callback to indicate the destination file has changed.
TimeDuration getEffectsLength() const
Returns the length of the effect.
TimeRange getEffectsRange() const
Returns the range of the file that the effect should apply to.
double getSpeedRatioEstimate() const
Returns the speed ratio of the clip or an estimate of this if the clip is auto tempo.
juce::ValueTree state
The ValueTree of the Clip state.
const EditItemID itemID
Every EditItem has an ID which is unique within the edit.
The Tracktion Edit class!
PluginCache & getPluginCache() noexcept
Returns the PluginCache which manages all active Plugin[s] for this Edit.
TempoSequence tempoSequence
The global TempoSequence of this Edit.
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.
The Engine is the central class for all tracktion sessions.
AudioFileFormatManager & getAudioFileFormatManager() const
Returns the AudioFileFormatManager that maintains a list of available audio file formats.
UIBehaviour & getUIBehaviour() const
Returns the UIBehaviour class.
An AudioNode that fades its input node in/out at given times.
The base class that all generator jobs derive from.
Manages a list of items that are currently selected.
An AudioNode that speeds up and slows down its input node in/out at given times.
virtual Plugin::Ptr showMenuAndCreatePlugin(Plugin::Type, Edit &)
Should show the new plugin window and creates the Plugin the user selects.
The built-in Tracktion volume/pan plugin.
bool renderNextBlock() override
During a render process this will be repeatedly called.
A WarpTimeManager contains a list of WarpMarkers and some source material and maps times from a linea...
TimePosition getWarpEndMarkerTime() const
Sets position in warped region of the redered file end point.
An AudioNode that plays back a wave file.
T get(T... args)
T is_pointer_v
#define TRANS(stringLiteral)
#define jassert(expression)
#define JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(className)
#define jassertfalse
typedef int
typedef float
T max(T... args)
T min(T... args)
constexpr Type jmax(Type a, Type b)
Type jlimit(Type lowerLimit, Type upperLimit, Type valueToConstrain) noexcept
int roundToInt(const FloatType value) noexcept
long long int64
Declarations from this namespaces are inlined into tracktion.
legacy::EditTimeRange toEditTimeRange(TimeRange r)
@temporary
bool matchesAnyOf(const Type &needle, const std::initializer_list< Type > &haystack)
Returns true if the needle is found in the haystack.
Holds some really basic properties of a node.
Passed into AudioNodes when they are being initialised, to give them useful contextual information th...
Represents a duration in real-life time.
Represents a position in real-life time.
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.
Takes an AudioNode and renders it to a file.
bool completeRender() override
This is called once after all the render blocks have completed.
bool renderNextBlock() override
During a render process this will be repeatedly called.
bool setUpRender() override
Subclasses should override this to set-up their render process.
bool setUpRender() override
Subclasses should override this to set-up their render process.
bool completeRender() override
This is called once after all the render blocks have completed.
virtual bool completeRender()=0
This is called once after all the render blocks have completed.
virtual bool setUpRender()=0
Subclasses should override this to set-up their render process.
virtual bool renderNextBlock()=0
During a render process this will be repeatedly called.
juce::ReferenceCountedObjectPtr< ClipEffectRenderJob > createRenderJob(const AudioFile &sourceFile, double sourceLength) override
Subclasses should return a job that can render the source.
bool renderNextBlock() override
During a render process this will be repeatedly called.
juce::ReferenceCountedObjectPtr< ClipEffect::ClipEffectRenderJob > createRenderJob(const AudioFile &, double sourceLength) override
Subclasses should return a job that can render the source.
bool renderNextBlock() override
During a render process this will be repeatedly called.
bool setUpRender() override
Subclasses should override this to set-up their render process.
bool hasProperties() override
Return true here to show a properties button in the editor and enable the propertiesButtonPressed cal...
juce::ReferenceCountedObjectPtr< ClipEffectRenderJob > createRenderJob(const AudioFile &, double sourceLength) override
Subclasses should return a job that can render the source.
juce::String getSelectableDescription() override
Subclasses must return a description of what they are.
bool renderNextBlock() override
During a render process this will be repeatedly called.
juce::ReferenceCountedObjectPtr< ClipEffectRenderJob > createRenderJob(const AudioFile &sourceFile, double sourceLength) override
Subclasses should return a job that can render the source.
bool hasProperties() override
Return true here to show a properties button in the editor and enable the propertiesButtonPressed cal...
juce::String getSelectableDescription() override
Subclasses must return a description of what they are.
bool hasProperties() override
Return true here to show a properties button in the editor and enable the propertiesButtonPressed cal...
juce::ReferenceCountedObjectPtr< ClipEffectRenderJob > createRenderJob(const AudioFile &sourceFile, double sourceLength) override
Subclasses should return a job that can render the source.
void curveHasChanged(AutomatableParameter &) override
Called when the automation curve has changed, point time, value or curve.
bool hasProperties() override
Return true here to show a properties button in the editor and enable the propertiesButtonPressed cal...
juce::ReferenceCountedObjectPtr< ClipEffectRenderJob > createRenderJob(const AudioFile &, double sourceLength) override
Subclasses should return a job that can render the source.
bool renderNextBlock() override
During a render process this will be repeatedly called.
juce::ReferenceCountedObjectPtr< ClipEffectRenderJob > createRenderJob(const AudioFile &, double sourceLength) override
Subclasses should return a job that can render the source.
juce::ReferenceCountedObjectPtr< ClipEffectRenderJob > createRenderJob(const AudioFile &sourceFile, double sourceLength) override
Subclasses should return a job that can render the source.
juce::String getSelectableDescription() override
Subclasses must return a description of what they are.
bool hasProperties() override
Return true here to show a properties button in the editor and enable the propertiesButtonPressed cal...
juce::ReferenceCountedObjectPtr< ClipEffectRenderJob > createRenderJob(const AudioFile &sourceFile, double sourceLength) override
Subclasses should return a job that can render the source.
bool hasProperties() override
Return true here to show a properties button in the editor and enable the propertiesButtonPressed cal...
void sourceChanged() override
Callback to indicate the destination file has changed.
juce::ReferenceCountedObjectPtr< ClipEffectRenderJob > createRenderJob(const AudioFile &, double sourceLength) override
Subclasses should return a job that can render the source.
#define CRASH_TRACER
This macro adds the current location to a stack which gets logged if a crash happens.