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

« « « Anklang Documentation
Loading...
Searching...
No Matches
tracktion_AudioTrack.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
14//==============================================================================
16{
17 TrackMuter (AudioTrack& at) : owner (at) { triggerAsyncUpdate(); }
18 ~TrackMuter() override { cancelPendingUpdate(); }
19
20 void handleAsyncUpdate() override
21 {
22 for (int i = 1; i <= 16; ++i)
23 owner.injectLiveMidiMessage (juce::MidiMessage::allNotesOff (i),
24 MidiMessageArray::notMPE);
25
26 owner.trackMuter = nullptr;
27 }
28
29 AudioTrack& owner;
31};
32
33//==============================================================================
35 private juce::AsyncUpdater
36{
38 : owner (at), state (owner.state), triggerFreeze (false), updateFreeze (false)
39 {
40 state.addListener (this);
41 }
42
43 ~FreezeUpdater() override
44 {
45 state.removeListener (this);
47 }
48
49 void freeze()
50 {
51 markAndUpdate (triggerFreeze);
52 }
53
54 AudioTrack& owner;
55 juce::ValueTree state;
56
57private:
58 bool triggerFreeze, updateFreeze;
59
60 void markAndUpdate (bool& flag) { flag = true; triggerAsyncUpdate(); }
61
62 bool compareAndReset (bool& flag)
63 {
64 if (! flag)
65 return false;
66
67 flag = false;
68 return true;
69 }
70
71 void valueTreeChanged() override
72 {
73 markAndUpdate (updateFreeze);
74 }
75
76 void handleAsyncUpdate() override
77 {
78 if (compareAndReset (triggerFreeze))
79 {
80 if (! owner.isFrozen (groupFreeze))
82 }
83
84 if (compareAndReset (updateFreeze))
85 {
86 if (owner.isFrozen (Track::individualFreeze) && (! owner.hasFreezePointPlugin()))
87 owner.setFrozen (false, individualFreeze);
88 }
89 }
90
92};
93
94//==============================================================================
95AudioTrack::AudioTrack (Edit& ed, const juce::ValueTree& v)
96 : ClipTrack (ed, v, true),
98{
99 soloed.referTo (state, IDs::solo, nullptr);
100 soloIsolated.referTo (state, IDs::soloIsolate, nullptr);
101 muted.referTo (state, IDs::mute, nullptr);
102 frozen.referTo (state, IDs::frozen, nullptr);
103 frozenIndividually.referTo (state, IDs::frozenIndividually, nullptr);
104 ghostTracks.referTo (state, IDs::ghostTracks, nullptr);
105 maxInputs.referTo (state, IDs::maxInputs, nullptr, 1);
106 compGroup.referTo (state, IDs::compGroup, &edit.getUndoManager(), -1);
107 midiNoteMap.referTo (state, IDs::midiNoteMap, nullptr);
108 playSlotClips.referTo (state, IDs::playSlotClips, nullptr);
109
110 updateMidiNoteMapCache();
111
113 desc.name = itemID.toString();
114 desc.channels = { ChannelIndex (0, juce::AudioChannelSet::left),
116
117 callBlocking ([this, desc]
118 {
119 waveInputDevice = std::make_unique<WaveInputDevice> (edit.engine, TRANS("Track Wave Input"),
120 desc, InputDevice::trackWaveDevice);
121
122 midiInputDevice = std::make_unique<VirtualMidiInputDevice> (edit.engine, itemID.toString(),
123 InputDevice::trackMidiDevice,
124 "TrkMIDI_" + itemID.toString(),
125 false);
126
127 auto& eid = edit.getEditInputDevices();
128 waveInputDevice->setEnabled (eid.isInputDeviceAssigned (*waveInputDevice));
129 midiInputDevice->setEnabled (eid.isInputDeviceAssigned (*midiInputDevice));
130 });
131
132 output = std::make_unique<TrackOutput> (*this);
133 freezeUpdater = std::make_unique<FreezeUpdater> (*this);
134
135 if (getMidiVerticalOffset() < 0 || getMidiVerticalOffset() > 0.99
137 setVerticalScaleToDefault();
138
139 asyncCaller.addFunction (updateAutoCrossfadesFlag,
140 [this]
141 {
142 for (auto c : getClips())
143 if (auto acb = dynamic_cast<AudioClipBase*> (c))
144 if (acb->getAutoCrossfade())
145 acb->updateAutoCrossfadesAsync (false);
146 });
147}
148
149AudioTrack::~AudioTrack()
150{
151 const bool clearWave = waveInputDevice != nullptr && waveInputDevice->isEnabled();
152 const bool clearMidi = midiInputDevice != nullptr && midiInputDevice->isEnabled();
153
154 if (clearWave || clearMidi)
155 {
157
158 for (auto at : getTracksOfType<AudioTrack> (edit, true))
159 {
160 if (clearWave) edit.getEditInputDevices().clearInputsOfDevice (*at, *waveInputDevice, &edit.getUndoManager());
161 if (clearMidi) edit.getEditInputDevices().clearInputsOfDevice (*at, *midiInputDevice, &edit.getUndoManager());
162 }
163 }
164
165 notifyListenersOfDeletion();
166}
167
169{
171
173
174 if (! edit.isLoading())
176
177 if (frozenIndividually && ! getFreezeFile().existsAsFile())
179
180 output->initialise();
181}
182
184{
185 auto n = ClipTrack::getName();
186
187 if ((n.startsWithIgnoreCase ("Track ")
188 || n.startsWithIgnoreCase (TRANS("Track") + " "))
189 && n.substring (6).trim().containsOnly ("0123456789"))
190 {
191 // For "Track 1" type names, leave the actual name empty.
192 resetName();
193 changed();
194 }
195
196 // We need to update the alias here as it's the first time the name will be returned correctly upon track creation
197 // We also won't get a property change if the track order etc. changes as it will just be empty
198 auto devName = getName();
199
200 if (waveInputDevice != nullptr) waveInputDevice->setAlias (devName);
201 if (midiInputDevice != nullptr) midiInputDevice->setAlias (devName);
202}
203
204juce::String AudioTrack::getName() const
205{
206 if (auto n = ClipTrack::getName(); ! n.isEmpty())
207 return n;
208
209 return getNameAsTrackNumber();
210}
211
212int AudioTrack::getAudioTrackNumber() const noexcept
213{
214 int result = 1;
215
216 edit.visitAllTracksRecursive ([&] (Track& t)
217 {
218 if (this == &t)
219 return false;
220
221 if (t.isAudioTrack())
222 ++result;
223
224 return true;
225 });
226
227 return result;
228}
229
231{
232 return TRANS("Track") + " " + juce::String (getAudioTrackNumber());
233}
234
236{
237 auto desc = getNameAsTrackNumber();
238
239 if (auto n = getName(); ! n.startsWithIgnoreCase (TRANS("Track") + " "))
240 desc << " (" << n << ")";
241
242 return desc;
243}
244
249
250VolumeAndPanPlugin* AudioTrack::getVolumePlugin() { return pluginList.getPluginsOfType<VolumeAndPanPlugin>().getLast(); }
251LevelMeterPlugin* AudioTrack::getLevelMeterPlugin() { return pluginList.getPluginsOfType<LevelMeterPlugin>().getLast(); }
252EqualiserPlugin* AudioTrack::getEqualiserPlugin() { return pluginList.getPluginsOfType<EqualiserPlugin>().getLast(); }
253
254AuxSendPlugin* AudioTrack::getAuxSendPlugin (int bus, AuxPosition ap) const
255{
256 if (ap == AuxPosition::byBus)
257 {
258 for (auto p : pluginList)
259 if (auto f = dynamic_cast<AuxSendPlugin*> (p))
260 if (bus < 0 || bus == f->getBusNumber())
261 return f;
262 }
263 else if (ap == AuxPosition::byPosition)
264 {
265 jassert(bus >= 0);
266
267 int idx = 0;
268 for (auto p : pluginList)
269 {
270 if (auto f = dynamic_cast<AuxSendPlugin*> (p))
271 {
272 if (idx == bus)
273 return f;
274
275 idx++;
276 }
277 }
278 }
279
280 return {};
281}
282
283//==============================================================================
284juce::String AudioTrack::getNameForMidiNoteNumber (int note, int midiChannel, bool preferSharp) const
285{
286 jassert (midiChannel > 0);
287
288 if (midiNoteMapCache.size() > 0)
289 {
290 auto itr = midiNoteMapCache.find (note);
291
292 if (itr != midiNoteMapCache.end())
293 return itr->second;
294
295 return {};
296 }
297
298 juce::String s;
299
300 for (auto af : pluginList)
301 if (af->hasNameForMidiNoteNumber (note, midiChannel, s))
302 return s;
303
304 if (auto dest = output->getDestinationTrack())
305 return dest->getNameForMidiNoteNumber (note, midiChannel, preferSharp);
306
307 // try the master plugins..
308 for (auto af : edit.getMasterPluginList())
309 if (af->hasNameForMidiNoteNumber (note, midiChannel, s))
310 return s;
311
312 if (auto mo = dynamic_cast<MidiOutputDevice*> (getOutput().getOutputDevice (true)))
313 return mo->getNameForMidiNoteNumber (note, midiChannel, preferSharp);
314
315 return midiChannel == 10 ? TRANS(juce::MidiMessage::getRhythmInstrumentName (note))
316 : juce::MidiMessage::getMidiNoteName (note, preferSharp, true,
317 edit.engine.getEngineBehaviour().getMiddleCOctave());
318}
319
320void AudioTrack::updateMidiNoteMapCache()
321{
322 midiNoteMapCache.clear();
323
324 auto m = midiNoteMap.get();
325
326 auto lines = juce::StringArray::fromLines (m);
327 for (auto l : lines)
328 {
329 if (l.startsWith ("//"))
330 continue;
331
332 int digits = 0;
333 for (int i = 0; i < l.length(); i++)
334 {
335 auto c = l[i];
336
338 digits++;
339 else
340 break;
341 }
342
343 if (digits > 0)
344 midiNoteMapCache[l.substring (0, digits).getIntValue()] = l.substring (digits).trim();
345 }
346}
347
348bool AudioTrack::areMidiPatchesZeroBased() const
349{
350 // do something for plugins here
351 if (auto dest = output->getDestinationTrack())
352 return dest->areMidiPatchesZeroBased();
353
354 // try the master plugins..
355 if (auto midiDevice = dynamic_cast<MidiOutputDevice*> (output->getOutputDevice (false)))
356 return midiDevice->areMidiPatchesZeroBased();
357
358 return false;
359}
360
361juce::String AudioTrack::getNameForBank (int bank) const
362{
363 juce::String s;
364
365 for (auto p : pluginList)
366 if (p->hasNameForMidiBank (bank, s))
367 return s;
368
369 if (auto dest = output->getDestinationTrack())
370 return dest->getNameForBank (bank);
371
372 // try the master plugins..
373 for (auto p : edit.getMasterPluginList())
374 if (p->hasNameForMidiBank (bank, s))
375 return s;
376
377 if (auto midiDevice = dynamic_cast<MidiOutputDevice*> (output->getOutputDevice (false)))
378 return midiDevice->getBankName (bank);
379
380 return edit.engine.getMidiProgramManager().getBankName (0, bank);
381}
382
383int AudioTrack::getIdForBank (int bank) const
384{
385 if (auto dest = output->getDestinationTrack())
386 return dest->getIdForBank (bank);
387
388 if (auto midiDevice = dynamic_cast<MidiOutputDevice*> (output->getOutputDevice (false)))
389 return midiDevice->getBankID (bank);
390
391 return bank;
392}
393
394juce::String AudioTrack::getNameForProgramNumber (int programNumber, int bank) const
395{
396 juce::String s;
397
398 for (auto p : pluginList)
399 if (p->hasNameForMidiProgram (programNumber, bank, s))
400 return s;
401
402 if (const AudioTrack* const dest = output->getDestinationTrack())
403 return dest->getNameForProgramNumber (programNumber, bank);
404
405 // try the master plugins..
406 for (auto p : edit.getMasterPluginList())
407 if (p->hasNameForMidiProgram (programNumber, bank, s))
408 return s;
409
410 if (auto midiDevice = dynamic_cast<MidiOutputDevice*> (output->getOutputDevice (false)))
411 return midiDevice->getProgramName (programNumber, bank);
412
413 return TRANS(juce::MidiMessage::getGMInstrumentName (programNumber));
414}
415
416//==============================================================================
417void AudioTrack::setMute (bool b) { muted = b; }
418void AudioTrack::setSolo (bool b) { soloed = b; }
419void AudioTrack::setSoloIsolate (bool b) { soloIsolated = b; }
420
421bool AudioTrack::isMuted (bool includeMutingByDestination) const
422{
423 if (muted)
424 return true;
425
426 if (includeMutingByDestination)
427 {
428 if (auto p = getParentFolderTrack())
429 return p->isMuted (true);
430
431 if (auto dest = output->getDestinationTrack())
432 return dest->isMuted (true);
433 }
434
435 return false;
436}
437
438bool AudioTrack::isSolo (bool includeIndirectSolo) const
439{
440 if (soloed)
441 return true;
442
443 if (includeIndirectSolo)
444 {
445 // If any of the parent tracks are soloed, this needs to be indirectly soloed
446 for (auto p = getParentFolderTrack(); p != nullptr; p = p->getParentFolderTrack())
447 if (p->isSolo (false))
448 return true;
449
450 if (! isPartOfSubmix())
451 if (auto dest = output->getDestinationTrack())
452 return dest->isSolo (true);
453 }
454
455 return false;
456}
457
458bool AudioTrack::isSoloIsolate (bool includeIndirectSolo) const
459{
460 if (soloIsolated)
461 return true;
462
463 if (includeIndirectSolo)
464 {
465 // If any of the parent tracks are solo isolate, this needs to be indirectly solo isolate
466 for (auto p = getParentFolderTrack(); p != nullptr; p = p->getParentFolderTrack())
467 if (p->isSoloIsolate (false))
468 return true;
469
470 if (! isPartOfSubmix())
471 if (auto dest = output->getDestinationTrack())
472 return dest->isSoloIsolate (true);
473 }
474
475 return false;
476}
477
478static bool isInputTrackSolo (const Track& track)
479{
480 for (auto t : track.getInputTracks())
481 if (t->isSolo (true))
482 return true;
483
484 return false;
485}
486
487bool AudioTrack::isTrackAudible (bool areAnyTracksSolo) const
488{
489 if (areAnyTracksSolo && isInputTrackSolo (*this))
490 return true;
491
492 return Track::isTrackAudible (areAnyTracksSolo);
493}
494
495//==============================================================================
497{
498 bool hasMidi = false, hasWave = false;
499
500 for (auto c : getClips())
501 {
502 auto type = c->type;
503
504 if (! hasMidi && (type == TrackItem::Type::midi || type == TrackItem::Type::step))
505 hasMidi = true;
506
507 if (! hasWave && type == TrackItem::Type::wave)
508 hasWave = true;
509
510 if (hasMidi && hasWave)
511 break;
512 }
513
514 if (hasMidi && ! canPlayMidi())
515 return TRANS("This track contains MIDI-generating clips which may be inaudible as it doesn't output to a MIDI device or a plugin synthesiser.")
516 + "\n\n" + TRANS("To change a track's destination, select the track and use its destination list.");
517
518 if (hasWave && ! canPlayAudio())
519 {
520 if (! getOutput().canPlayAudio())
521 return TRANS("This track contains wave clips which may be inaudible as it doesn't output to an audio device.")
522 + "\n\n" + TRANS("To change a track's destination, select the track and use its destination list.");
523
524 return TRANS("This track contains wave clips which may be inaudible as the audio will be blocked by some of the track's plugins.");
525 }
526
527 return {};
528}
529
530juce::String AudioTrack::getLauncherPlayabilityWarning() const
531{
532 bool hasMidi = false, hasWave = false;
533
534 if (clipSlotList)
535 {
536 for (auto slot : clipSlotList->getClipSlots())
537 {
538 if (slot)
539 {
540 if (auto c = slot->getClip())
541 {
542 auto type = c->type;
543
544 if (! hasMidi && (type == TrackItem::Type::midi || type == TrackItem::Type::step))
545 hasMidi = true;
546
547 if (! hasWave && type == TrackItem::Type::wave)
548 hasWave = true;
549
550 if (hasMidi && hasWave)
551 break;
552 }
553 }
554 }
555 }
556
557 if (hasMidi && ! canPlayMidi())
558 return TRANS("This track contains MIDI-generating clips which may be inaudible as it doesn't output to a MIDI device or a plugin synthesiser.")
559 + "\n\n" + TRANS("To change a track's destination, select the track and use its destination list.");
560
561 if (hasWave && ! canPlayAudio())
562 {
563 if (! getOutput().canPlayAudio())
564 return TRANS("This track contains wave clips which may be inaudible as it doesn't output to an audio device.")
565 + "\n\n" + TRANS("To change a track's destination, select the track and use its destination list.");
566
567 return TRANS("This track contains wave clips which may be inaudible as the audio will be blocked by some of the track's plugins.");
568 }
569
570 return {};
571}
572
574{
575 if (! getOutput().canPlayAudio())
576 return false;
577
578 for (auto p : pluginList)
579 if (! p->takesAudioInput())
580 return false;
581
582 return true;
583}
584
585bool AudioTrack::canPlayMidi() const
586{
587 if (getOutput().canPlayMidi())
588 return true;
589
590 for (auto p : pluginList)
591 if (p->takesMidiInput())
592 return getOutput().canPlayAudio();
593
594 if (isPartOfSubmix())
595 for (auto ft = getParentFolderTrack(); ft != nullptr; ft = ft->getParentFolderTrack())
596 for (auto p : ft->pluginList)
597 if (p->takesMidiInput())
598 return getOutput().canPlayAudio();
599
600 return false;
601}
602
603//==============================================================================
605{
606 if (! clipSlotList)
607 clipSlotList = std::make_unique<ClipSlotList> (state.getOrCreateChildWithName (IDs::CLIPSLOTS, &edit.getUndoManager()), *this);
608
609 return *clipSlotList;
610}
611
612//==============================================================================
613double AudioTrack::getMidiVerticalOffset() const
614{
615 return state.getProperty (IDs::midiVOffset, juce::var (defaultMidiVerticalOffset));
616}
617
619{
620 return state.getProperty (IDs::midiVProp, juce::var (defaultMidiVisibleProportion));
621}
622
623void AudioTrack::setMidiVerticalPos (double visibleProp, double offset)
624{
625 visibleProp = juce::jlimit (0.0, 1.0, visibleProp);
626 auto vo = juce::jlimit (0.0, 1.0 - visibleProp, offset);
627 auto vp = juce::jlimit (0.1, 1.0 - vo, visibleProp);
628
629 state.setProperty (IDs::midiVOffset, vo, nullptr);
630 state.setProperty (IDs::midiVProp, vp, nullptr);
631}
632
633void AudioTrack::setVerticalScaleToDefault()
634{
635 auto midiNotes = juce::jlimit (1, 128, 12 * static_cast<int> (edit.engine.getPropertyStorage()
636 .getProperty (SettingID::midiEditorOctaves, 3)));
637
638 defaultMidiVisibleProportion = midiNotes / 128.0;
639 defaultMidiVerticalOffset = (1.0 - defaultMidiVisibleProportion) * 0.5;
640
641 state.removeProperty (IDs::midiVOffset, nullptr);
642 state.removeProperty (IDs::midiVProp, nullptr);
643}
644
645void AudioTrack::setTrackToGhost (AudioTrack* track, bool shouldGhost)
646{
647 if (track == nullptr)
648 return;
649
650 auto list = EditItemID::parseStringList (ghostTracks.get());
651 bool isGhosted = list.contains (track->itemID);
652
653 if (isGhosted != shouldGhost)
654 {
655 if (shouldGhost)
656 list.add (track->itemID);
657 else
658 list.removeAllInstancesOf (track->itemID);
659
660 ghostTracks = EditItemID::listToString (list);
661 }
662}
663
664juce::Array<AudioTrack*> AudioTrack::getGhostTracks() const
665{
666 if (ghostTracks.get().isEmpty())
667 return {};
668
670
671 for (auto& trackID : EditItemID::parseStringList (ghostTracks))
672 if (auto at = findAudioTrackForID (edit, trackID))
673 tracks.add (at);
674
675 return tracks;
676}
677
678void AudioTrack::playGuideNote (int note, MidiChannel midiChannel, int velocity, bool stopOtherFirst, bool forceNote, bool autorelease)
679{
680 jassert (midiChannel.isValid()); //SysEx?
681 jassert (velocity >= 0 && velocity <= 127);
682
683 if (stopOtherFirst)
684 turnOffGuideNotes (midiChannel);
685
686 if (note >= 0 && (forceNote || edit.engine.getEngineBehaviour().shouldPlayMidiGuideNotes()))
687 {
688 const int pitch = juce::jlimit (0, 127, note);
689
690 if (! currentlyPlayingGuideNotes.contains (pitch))
691 {
692 currentlyPlayingGuideNotes.add (pitch);
693 injectLiveMidiMessage (juce::MidiMessage::noteOn (midiChannel.getChannelNumber(),
694 pitch, (uint8_t) velocity),
695 MidiMessageArray::notMPE);
696 }
697
698 if (autorelease)
699 startTimer (100);
700 }
701}
702
703void AudioTrack::playGuideNotes (const juce::Array<int>& notes, MidiChannel midiChannel,
704 const juce::Array<int>& vels, bool stopOthersFirst)
705{
706 jassert (midiChannel.isValid()); //SysEx?
707
708 if (stopOthersFirst)
709 turnOffGuideNotes (midiChannel);
710
711 if (notes.size() < 8 && edit.engine.getEngineBehaviour().shouldPlayMidiGuideNotes())
712 {
713 for (int i = 0; i < notes.size(); ++i)
714 {
715 const int pitch = juce::jlimit (0, 127, notes.getUnchecked (i));
716
717 if (! currentlyPlayingGuideNotes.contains (pitch))
718 {
719 currentlyPlayingGuideNotes.add (pitch);
720 injectLiveMidiMessage (juce::MidiMessage::noteOn (midiChannel.getChannelNumber(),
721 pitch, (uint8_t) vels.getUnchecked (i)),
722 MidiMessageArray::notMPE);
723 }
724 }
725 }
726}
727
728void AudioTrack::turnOffGuideNotes()
729{
730 stopTimer();
731
732 for (int ch = 1; ch <= 16; ch++)
733 turnOffGuideNotes (MidiChannel (ch));
734}
735
736void AudioTrack::turnOffGuideNotes (MidiChannel midiChannel)
737{
738 jassert (midiChannel.isValid()); //SysEx?
739 auto channel = midiChannel.getChannelNumber();
740
741 for (auto note : currentlyPlayingGuideNotes)
742 injectLiveMidiMessage (juce::MidiMessage::noteOff (channel, note),
743 MidiMessageArray::notMPE);
744
745 currentlyPlayingGuideNotes.clear();
746}
747
748//==============================================================================
750{
751 if (listeners.isEmpty())
752 edit.restartPlayback();
753
754 listeners.add (l);
755}
756
758{
759 listeners.remove (l);
760 // N.B. Don't call restartPlayback here or it will be impossible to clear the audio graph
761}
762
763//==============================================================================
764void AudioTrack::valueTreePropertyChanged (juce::ValueTree& v, const juce::Identifier& i)
765{
766 if (v == state)
767 {
768 if (i == IDs::maxInputs)
769 {
770 maxInputs.forceUpdateOfCachedValue();
771 edit.getEditInputDevices().clearAllInputs (*this, &edit.getUndoManager());
772 }
773 else if (i == IDs::ghostTracks)
774 {
775 if (ghostTracks.get().isEmpty())
776 ghostTracks.resetToDefault();
777
778 changed();
779 }
780 else if (i == IDs::playSlotClips)
781 {
782 playSlotClips.forceUpdateOfCachedValue();
783
784 if (! playSlotClips.get())
785 for (auto cs : getClipSlotList().getClipSlots())
786 if (auto c = cs->getClip())
787 if (auto lh = c->getLaunchHandle(); lh->getPlayingStatus() == LaunchHandle::PlayState::playing)
788 lh->stop ({});
789 }
790 else if (i == IDs::compGroup)
791 {
792 changed();
793 }
794 else if (i == IDs::frozen)
795 {
797 changed();
798 }
799 else if (i == IDs::frozenIndividually)
800 {
801 frozenIndividually.forceUpdateOfCachedValue();
802
803 if (frozenIndividually)
804 freezeTrack();
805 else
806 unFreezeTrack();
807 }
808 else if (i == IDs::name)
809 {
810 auto devName = getName();
811
812 waveInputDevice->setAlias (devName);
813 midiInputDevice->setAlias (devName);
814 }
815 else if (i == IDs::midiNoteMap)
816 {
817 updateMidiNoteMapCache();
818 }
819 }
820 else if (Clip::isClipState (v))
821 {
822 TRACKTION_ASSERT_MESSAGE_THREAD;
823
824 if (i == IDs::start || i == IDs::length)
825 asyncCaller.updateAsync (updateAutoCrossfadesFlag);
826
827 if (i == IDs::mute && bool (v.getProperty (i)))
828 if (trackMuter == nullptr && ! bool (v.getProperty (IDs::processMidiWhenMuted, false)))
829 if (v.hasType (IDs::MIDICLIP))
830 trackMuter = std::make_unique<TrackMuter> (*this);
831 }
832
834}
835
836void AudioTrack::valueTreeParentChanged (juce::ValueTree& v)
837{
839
840 if (state.getParent().isValid())
841 if (int numScenes = getClipSlotList().getClipSlots().size(); numScenes > 0)
842 edit.getSceneList().ensureNumberOfScenes (getClipSlotList().getClipSlots().size());
843}
844
845//==============================================================================
846bool AudioTrack::hasAnyLiveInputs()
847{
848 for (auto in : edit.getAllInputDevices())
849 if (in->isRecordingActive (itemID) && in->getTargets().contains (itemID))
850 return true;
851
852 return false;
853}
854
855bool AudioTrack::hasAnyTracksFeedingIn()
856{
857 for (auto t : getAudioTracks (edit))
858 if (t != this && t->getOutput().feedsInto (this))
859 return true;
860
861 return false;
862}
863
864//==============================================================================
865void AudioTrack::injectLiveMidiMessage (const MidiMessageArray::MidiMessageWithSource& message)
866{
867 TRACKTION_ASSERT_MESSAGE_THREAD
868 bool wasUsed = false;
869 listeners.call (&Listener::injectLiveMidiMessage, *this, message, wasUsed);
870
871 if (! wasUsed)
872 edit.warnOfWastedMidiMessages (nullptr, this);
873}
874
875void AudioTrack::injectLiveMidiMessage (const juce::MidiMessage& m, MidiMessageArray::MPESourceID source)
876{
877 injectLiveMidiMessage ({ m, source });
878}
879
881 MidiClip* mc, MidiList::NoteAutomationType automationType)
882{
883
884 if (mc == nullptr)
885 {
886 const auto start = TimePosition::fromSeconds (ms.getStartTime()) + toDuration (startTime);
887 const auto end = TimePosition::fromSeconds (ms.getEndTime()) + toDuration (startTime);
888
889 for (auto c : getClips())
890 {
891 if (c->getPosition().time.overlaps ({ start, end }))
892 {
893 mc = dynamic_cast<MidiClip*> (c);
894
895 if (mc != nullptr)
896 break;
897 }
898 }
899 }
900
901 if (mc != nullptr)
902 {
903 tracktion::mergeInMidiSequence (*mc, std::move (ms), toDuration (startTime), automationType);
904 return true;
905 }
906
907 return false;
908}
909
911{
912 juce::Array<Track*> inputTracks;
913
914 for (auto track : getAudioTracks (edit))
915 if (! track->isPartOfSubmix() && track != this && track->getOutput().feedsInto (this))
916 inputTracks.add (track);
917
918 for (auto track : getTracksOfType<FolderTrack> (edit, true))
919 if (! track->isPartOfSubmix() && track->getOutput() != nullptr && track->getOutput()->feedsInto (this))
920 inputTracks.add (track);
921
922 return inputTracks;
923}
924
925static bool canTrackBeChanged (InputDeviceInstance* idi)
926{
927 if (idi->isRecording())
928 {
929 idi->edit.engine.getUIBehaviour().showWarningMessage (TRANS("Can't change tracks whilst recording is active"));
930 return false;
931 }
932
933 return true;
934}
935
936void AudioTrack::setMaxNumOfInputs (int n)
937{
938 for (auto* idi : edit.getEditInputDevices().getDevicesForTargetTrack (*this))
939 if (! canTrackBeChanged (idi))
940 return;
941
942 maxInputs = n;
943}
944
945//==============================================================================
947{
948 return t == anyFreeze ? (frozen || frozenIndividually)
949 : (t == groupFreeze ? frozen : frozenIndividually);
950}
951
953{
954 if (type == individualFreeze)
955 {
956 if (frozenIndividually != b)
957 {
958 if (b && getOutput().getDestinationTrack() != nullptr)
959 {
960 edit.engine.getUIBehaviour().showWarningMessage (TRANS("Tracks which output to another track can't themselves be frozen; "
961 "instead, you should freeze the track they input into."));
962 }
963 else
964 {
965 frozenIndividually = b;
966 }
967 }
968 }
969 else
970 {
971 if (frozen != b)
972 {
973 if (! edit.isLoading())
974 {
975 const auto outputsToSubmixTrack = [this]
976 {
977 if (auto folder = getParentFolderTrack())
978 return folder->isSubmixFolder();
979
980 return false;
981 };
982
983 if (b && (getOutput().getDestinationTrack() != nullptr || outputsToSubmixTrack()))
984 {
985 edit.engine.getUIBehaviour().showWarningMessage (TRANS("Tracks which output to another track can't themselves be frozen; "
986 "instead, you should freeze the track they input into."));
987 }
988 else
989 {
990 frozen = b;
991 }
992 }
993 }
994 }
995}
996
998{
999 const bool isFreezePoint = dynamic_cast<FreezePointPlugin*> (p) != nullptr;
1000
1001 return dynamic_cast<VCAPlugin*> (p) == nullptr
1002 && (! isFreezePoint
1003 || (isFreezePoint && (p->getOwnerTrack() == this || ! hasFreezePointPlugin())));
1004}
1005
1006//==============================================================================
1007AudioTrack::FreezePointRemovalInhibitor::FreezePointRemovalInhibitor (AudioTrack& at) : track (at) { ++track.freezePointRemovalInhibitor; }
1008AudioTrack::FreezePointRemovalInhibitor::~FreezePointRemovalInhibitor() { --track.freezePointRemovalInhibitor; }
1009
1010void AudioTrack::freezeTrack()
1011{
1012 insertFreezePointIfRequired();
1013 const FreezePointPlugin::ScopedPluginDisabler spd (*this, juce::Range<int> (getIndexOfFreezePoint(),
1014 pluginList.size()));
1015
1016 auto& dm = edit.engine.getDeviceManager();
1017
1018 const bool shouldBeMuted = isMuted (true);
1019 setMute (false);
1020 const FreezePointPlugin::ScopedTrackUnsoloer stu (edit);
1021
1022 juce::BigInteger trackNum;
1023 trackNum.setBit (getIndexInEditTrackList());
1024 auto freezeFile = getFreezeFile();
1025 freezeFile.deleteFile();
1026
1027 juce::Array<EditItemID> trackIDs { itemID };
1028 juce::Array<Clip*> clips (getClips());
1029
1030 for (auto inputTrack : getInputTracks())
1031 {
1032 trackIDs.addIfNotAlreadyThere (inputTrack->itemID);
1033
1034 if (auto ct = dynamic_cast<ClipTrack*> (inputTrack))
1035 clips.addArray (ct->getClips());
1036 }
1037
1038 Renderer::Parameters r (edit);
1039 r.tracksToDo = trackNum;
1040 r.destFile = freezeFile;
1041 r.audioFormat = edit.engine.getAudioFileFormatManager().getFrozenFileFormat();
1042 r.blockSizeForAudio = dm.getBlockSize();
1043 r.sampleRateForAudio = dm.getSampleRate();
1044 r.time = { {}, getLengthIncludingInputTracks() };
1045 r.endAllowance = RenderOptions::findEndAllowance (edit, &trackIDs, nullptr);
1046 r.canRenderInMono = true;
1047 r.mustRenderInMono = false;
1048 r.usePlugins = true;
1049 r.useMasterPlugins = false;
1050 r.addAntiDenormalisationNoise = EditPlaybackContext::shouldAddAntiDenormalisationNoise (edit.engine);
1051 r.category = ProjectItem::Category::frozen;
1052
1053 const Edit::ScopedRenderStatus srs (edit, true);
1054 const auto desc = TRANS("Creating track freeze for \"XDVX\"")
1055 .replace ("XDVX", getName()) + "...";
1056
1057 if (getProjectForEdit (edit) != nullptr)
1059 else
1060 Renderer::renderToFile (desc, r);
1061
1062 freezePlugins (juce::Range<int> (0, getIndexOfFreezePoint()));
1063 setMute (shouldBeMuted);
1064
1065 if (! r.destFile.existsAsFile())
1066 {
1067 edit.engine.getUIBehaviour().showWarningMessage (TRANS("Nothing to freeze"));
1068 setFrozen (false, individualFreeze);
1069 return;
1070 }
1071
1072 changed();
1073}
1074
1075int AudioTrack::getIndexOfFreezePoint()
1076{
1077 int i = 0;
1078
1079 for (auto p : pluginList)
1080 {
1081 if (dynamic_cast<FreezePointPlugin*> (p) != nullptr)
1082 return i;
1083
1084 ++i;
1085 }
1086
1087 return -1;
1088}
1089
1090void AudioTrack::insertFreezePointAfterPlugin (const Plugin::Ptr& p)
1091{
1092 auto& pl = pluginList;
1093
1094 if (! pl.getPlugins().contains (p.get()))
1095 return;
1096
1097 removeFreezePoint();
1098 pl.insertPlugin (FreezePointPlugin::create(), pl.getPlugins().indexOf (p.get()) + 1);
1099
1101 // need to force the audio device to update before we start the render
1102}
1103
1104void AudioTrack::removeFreezePoint()
1105{
1106 auto& pl = pluginList;
1107
1108 for (int i = pl.size(); --i >= 0;)
1109 if (auto f = dynamic_cast<FreezePointPlugin*> (pl[i]))
1110 f->deleteFromParent();
1111}
1112
1113bool AudioTrack::insertFreezePointIfRequired()
1114{
1115 if (getIndexOfFreezePoint() != -1)
1116 return false;
1117
1118 if (auto p = pluginList.insertPlugin (FreezePointPlugin::create(), getIndexOfDefaultFreezePoint()))
1119 auto freezer = FreezePointPlugin::createTrackFreezer (p);
1120
1122 // need to force the audio device to update before we start the render
1123
1124 return true;
1125}
1126
1127void AudioTrack::freezeTrackAsync() const
1128{
1129 freezeUpdater->freeze();
1130}
1131
1132int AudioTrack::getIndexOfDefaultFreezePoint()
1133{
1134 int position = edit.engine.getPropertyStorage().getProperty (SettingID::freezePoint, 1);
1135
1136 if (position == FreezePointPlugin::beforeAllPlugins)
1137 return 0;
1138
1139 const auto& pl = pluginList;
1140
1141 for (int i = 0; i < pl.size(); ++i)
1142 {
1143 if (dynamic_cast<VolumeAndPanPlugin*> (pl[i]) != nullptr)
1144 {
1145 if (position == FreezePointPlugin::preFader)
1146 return i;
1147
1148 if (position == FreezePointPlugin::postFader)
1149 return i + 1;
1150 }
1151 }
1152
1153 return -1;
1154}
1155
1156void AudioTrack::freezePlugins (juce::Range<int> pluginsToFreeze)
1157{
1158 int i = 0;
1159
1160 for (auto p : pluginList)
1161 p->setFrozen (pluginsToFreeze.contains (i++));
1162}
1163
1164void AudioTrack::unFreezeTrack()
1165{
1166 // Remove the freeze point if it's in the default location as it will be put back there anyway
1167 int defaultPosition = edit.engine.getPropertyStorage().getProperty (SettingID::freezePoint, 0);
1168 auto defaultIndex = getIndexOfDefaultFreezePoint();
1169 auto freezePosition = (defaultPosition == FreezePointPlugin::postFader) ? defaultIndex
1170 : std::max (0, defaultIndex - 1);
1171
1172 if (getIndexOfFreezePoint() == freezePosition)
1173 if (freezePointRemovalInhibitor == 0)
1174 if (auto f = dynamic_cast<FreezePointPlugin*> (pluginList[freezePosition]))
1175 f->deleteFromParent();
1176
1177 // Unfreeze all plugins
1178 freezePlugins ({});
1179
1180 changed();
1181}
1182
1183juce::File AudioTrack::getFreezeFile() const
1184{
1185 return TemporaryFileManager::getFreezeFileForTrack (*this);
1186}
1187
1188bool AudioTrack::isSidechainSource() const
1189{
1190 for (auto p : edit.getPluginCache().getPlugins())
1191 if (p->getSidechainSourceID() == itemID)
1192 return true;
1193
1194 return false;
1195}
1196
1197juce::Array<Track*> AudioTrack::findSidechainSourceTracks() const
1198{
1199 juce::Array<Track*> srcTracks;
1200
1201 for (auto p : getAllPlugins())
1202 {
1203 auto srcId = p->getSidechainSourceID();
1204
1205 if (srcId.isValid())
1206 if (auto srcTrack = findTrackForID (edit, srcId))
1207 srcTracks.addIfNotAlreadyThere (srcTrack);
1208 }
1209
1210 return srcTracks;
1211}
1212
1213}} // namespace tracktion { inline namespace engine
ElementType getUnchecked(int index) const
int size() const noexcept
void add(const ElementType &newElement)
bool contains(ParameterType elementToLookFor) const
void clear()
bool addIfNotAlreadyThere(ParameterType newElement)
void cancelPendingUpdate() noexcept
BigInteger & setBit(int bitNumber)
void forceUpdateOfCachedValue()
Type get() const noexcept
static bool isDigit(char character) noexcept
double getStartTime() const noexcept
double getEndTime() const noexcept
static MidiMessage noteOn(int channel, int noteNumber, float velocity) noexcept
static const char * getGMInstrumentName(int midiInstrumentNumber)
static MidiMessage allNotesOff(int channel) noexcept
static String getMidiNoteName(int noteNumber, bool useSharps, bool includeOctaveNumber, int octaveNumForMiddleC)
static const char * getRhythmInstrumentName(int midiNoteNumber)
static StringArray fromLines(StringRef stringToBreakUp)
bool startsWithIgnoreCase(StringRef text) const noexcept
void stopTimer() noexcept
void startTimer(int intervalInMilliseconds) noexcept
virtual void valueTreeParentChanged(ValueTree &treeWhoseParentHasChanged)
virtual void valueTreePropertyChanged(ValueTree &treeWhosePropertyHasChanged, const Identifier &property)
bool isValid() const noexcept
ValueTree & setProperty(const Identifier &name, const var &newValue, UndoManager *undoManager)
void addListener(Listener *listener)
ValueTree getParent() const noexcept
const var & getProperty(const Identifier &name) const noexcept
void removeListener(Listener *listener)
ValueTree getOrCreateChildWithName(const Identifier &type, UndoManager *undoManager)
void removeProperty(const Identifier &name, UndoManager *undoManager)
Base class for Clips that produce some kind of audio e.g.
ClipSlotList & getClipSlotList()
Returns the ClipSlotList for this track.
bool canContainPlugin(Plugin *) const override
Returns true if this track can contain a specific Plugin.
bool mergeInMidiSequence(juce::MidiMessageSequence, TimePosition startTime, MidiClip *, MidiList::NoteAutomationType)
try to add this MIDI sequence to any MIDI clips that are already in the track.
bool canPlayAudio() const
checks whether audio clips can be played - i.e.
juce::CachedValue< AtomicWrapper< bool > > playSlotClips
Determines if the track's arrange clips or clip slots should be audible.
juce::String getNameForMidiNoteNumber(int note, int midiChannel, bool preferSharp=true) const
looks for a name for a midi note by trying all the plugins, and returning a default on failure.
bool isSolo(bool includeIndirectSolo) const override
Returns true if this track is soloed.
void initialise() override
Initialises the Track.
void sanityCheckName() override
checks whether the name is 'track n' and if so, makes sure the number is right
bool isTrackAudible(bool areAnyTracksSolo) const override
Returns whether this Track should be audible.
juce::String getTrackPlayabilityWarning() const
returns a warning message about this track not being playable, or "" if it's ok
bool isFrozen(FreezeType) const override
Returns true if this track is frozen using the given type.
void setSolo(bool) override
Subclasses should implement this to solo themselves.
void removeListener(Listener *)
Removes a Listener.
void addListener(Listener *)
Adds a Listener.
bool isMuted(bool includeMutingByDestination) const override
Returns true if this track is muted.
void setFrozen(bool, FreezeType) override
Attempts to freeze or unfreeze the track using a given FreezeType.
void setSoloIsolate(bool) override
Subclasses should implement this to solo isolate themselves.
void setMute(bool) override
Subclasses should implement this to mute themselves.
bool isSoloIsolate(bool includeIndirectSolo) const override
Returns true if this track is solo isolated.
double getMidiVisibleProportion() const
vertical scales for displaying the midi note editor
juce::String getNameAsTrackNumberWithDescription() const
Returns a name in the form "Track [number] ([track name])" (This is smart enough to not add the paren...
juce::String getSelectableDescription() override
Subclasses must return a description of what they are.
juce::String getNameForProgramNumber(int programNumber, int bank) const
prog number is 0 based.
juce::Array< Track * > getInputTracks() const override
Should return any tracks which feed into this track.
juce::String getNameAsTrackNumber() const
Returns a name in the form "Track [number]".
const juce::Array< Clip * > & getClips() const
Returns the clips this owner contains.
A list of the ClipSlots on a Track.
void ensureNumberOfSlots(int numSlots)
Adds Slots to ensure at least numSlots exist.
void initialise() override
Initialises the Track.
Plugin::Array getAllPlugins() const override
Returns all pugins on this Track.
static bool isClipState(const juce::ValueTree &)
Checks whether a ValueTree is some kind of clip state.
const EditItemID itemID
Every EditItem has an ID which is unique within the edit.
The Tracktion Edit class!
void restartPlayback()
Use this to tell the play engine to rebuild the audio graph if the toplogy has changed.
TransportControl & getTransport() const noexcept
Returns the TransportControl which is used to stop/stop/position playback and recording.
SceneList & getSceneList()
Returns a list of Scenes in the Edit.
void warnOfWastedMidiMessages(InputDevice *, Track *)
Triggers a callback to any registered WastedMidiMessagesListener[s].
EditInputDevices & getEditInputDevices() noexcept
Returns the EditInputDevices for the Edit.
void dispatchPendingUpdatesSynchronously()
If there's a change to send out to the listeners, do it now rather than waiting for the next timer me...
bool isLoading() const
Returns true if the Edit's not yet fully loaded.
void visitAllTracksRecursive(std::function< bool(Track &)>) const
Visits all tracks in the Edit with the given function.
juce::UndoManager & getUndoManager() noexcept
Returns the juce::UndoManager used for this Edit.
PluginList & getMasterPluginList() const noexcept
Returns the master PluginList.
Engine & engine
A reference to the Engine.
MidiProgramManager & getMidiProgramManager() const
Returns the MidiProgramManager instance that handles MIDI banks, programs, sets or presets.
PropertyStorage & getPropertyStorage() const
Returns the PropertyStorage user settings customisable XML file.
AudioFileFormatManager & getAudioFileFormatManager() const
Returns the AudioFileFormatManager that maintains a list of available audio file formats.
UIBehaviour & getUIBehaviour() const
Returns the UIBehaviour class.
DeviceManager & getDeviceManager() const
Returns the DeviceManager instance for handling audio / MIDI devices.
EngineBehaviour & getEngineBehaviour() const
Returns the EngineBehaviour instance.
An instance of an InputDevice that's available to an Edit.
Edit & edit
The Edit this instance belongs to.
virtual bool isRecording(EditItemID)=0
Returns true if there are any active recordings for this device.
Base class for elements which can contain macro parameters.
static ProjectItem::Ptr renderToProjectItem(const juce::String &taskDescription, const Parameters &params)
Renders an Edit to a file and creates a new ProjectItem for it.
static juce::File renderToFile(const juce::String &taskDescription, const Parameters &params)
Renders an Edit to a file given by the Parameters.
void ensureNumberOfScenes(int numScenes)
Adds Scenes to ensure numScenes are preset in the list.
int getNumScenes()
Returns the number of Scenes in the Edit.
virtual void changed()
This should be called to send a change notification to any SelectableListeners that are registered wi...
Base class for tracks which contain clips and plugins and can be added to Edit[s].
PluginList pluginList
The Track's PluginList.
FreezeType
Determines the type of freeze.
@ individualFreeze
Freezes a track in to a single audio file.
@ groupFreeze
Freezes multiple tracks together in to a single file.
@ anyFreeze
Either a group or individual freeze.
int getIndexInEditTrackList() const
Returns the index of this track in a flat list of tracks contained in an Edit.
bool isPartOfSubmix() const
Tests whether this nested within a submix FolderTrack.
juce::ValueTree state
The state of this Track.
bool hasFreezePointPlugin() const
Tests whether this Track contains a FreezePointPlugin.
void resetName()
Sets the name of the Track to an empty string.
FolderTrack * getParentFolderTrack() const
Returns the parent FolderTrack if this is nested in one.
virtual bool isTrackAudible(bool areAnyTracksSolo) const
Returns whether this Track should be audible.
void freePlaybackContext()
Detaches the current EditPlaybackContext, removing it from the DeviceManager.
virtual void showWarningMessage(const juce::String &message)
Should display a temporary warning message.
The VCA plugin sits on a folder track to control the overall level of all the volume/pan plugins in i...
The built-in Tracktion volume/pan plugin.
T clear(T... args)
T end(T... args)
T find(T... args)
T is_pointer_v
#define TRANS(stringLiteral)
#define jassert(expression)
#define JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(className)
T max(T... args)
Type jlimit(Type lowerLimit, Type upperLimit, Type valueToConstrain) noexcept
juce::Array< TrackType * > getTracksOfType(const Edit &, bool recursive)
Returns the tracks of a given type in an Edit.
juce::Array< AudioTrack * > getAudioTracks(const Edit &edit)
Returns all the AudioTracks in an Edit.
Track * findTrackForID(const Edit &edit, EditItemID id)
Returns the Track with a given ID if contained in the Edit.
Project::Ptr getProjectForEdit(const Edit &e)
Tries to find the project that contains this edit (but may return nullptr!)
void mergeInMidiSequence(MidiClip &mc, juce::MidiMessageSequence ms, TimeDuration startTime, MidiList::NoteAutomationType automationType)
Copies a zero-time origin based MIDI sequence in to a MidiClip.
AudioTrack * findAudioTrackForID(const Edit &edit, EditItemID id)
Returns the AudioTrack with a given ID if contained in the Edit.
T size(T... args)
Represents a position in real-life time.
void updateAsync(int functionID)
Triggers an asyncronous call to one of the functions.
Listener interface to be notified of recorded MIDI being sent to the plugins.
virtual void injectLiveMidiMessage(AudioTrack &, const MidiMessageArray::MidiMessageWithSource &, bool &wasUsed)=0
Called when a MidiMessage (i.e.
Describes a channel of a WaveInputDevice or WaveOutputDevice by specifying the channel index in the g...
Describes a WaveDevice from which the WaveOutputDevice and WaveInputDevice lists will be built.
#define CRASH_TRACER
This macro adds the current location to a stack which gets logged if a crash happens.