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

« « « Anklang Documentation
Loading...
Searching...
No Matches
tracktion_MidiInputDevice.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//==============================================================================
15//==============================================================================
21{
22public:
23 NoteDispatcher (MidiInputDevice& o) : owner (o)
24 {
25 startTimer (1);
26 items.reserve (20);
27 }
28
29 ~NoteDispatcher() override
30 {
31 stopTimer();
32 }
33
34 void hiResTimerCallback() override
35 {
36 juce::ScopedLock sl (lock);
37
38 if (items.size() == 0)
39 return;
40
42
43 int sent = 0;
44 for (size_t i = 0; i < items.size(); i++)
45 {
46 if (items[i].when > now)
47 break;
48
49 auto m = items[i].m;
50 m.setTimeStamp (juce::Time::getMillisecondCounterHiRes() * 0.001);
51
52 owner.handleIncomingMidiMessage (m);
53
54 sent++;
55 }
56
57 if (sent > 0)
58 items.erase (items.begin(), items.begin() + sent);
59 }
60
61 void enqueue (double tm, const juce::MidiMessage& m)
62 {
63 juce::ScopedLock sl (lock);
64
65 items.push_back ({tm, m});
66 }
67
68 void clear (int channel, int note)
69 {
70 juce::ScopedLock sl (lock);
71
72 items.erase (std::remove_if (items.begin(), items.end(),
73 [&](const Item& i) -> bool { return i.m.getChannel() == channel && i.m.getNoteNumber() == note; }),
74 items.end());
75 }
76
77private:
78 struct Item
79 {
80 double when;
82 };
83
86 MidiInputDevice& owner;
87};
88
89//==============================================================================
90//==============================================================================
96{
97public:
98 enum class HasFinished { no, yes };
99
100 RecordStopper (std::function<HasFinished (EditItemID)> checkTargetFinishedCallback)
101 : callback (std::move (checkTargetFinishedCallback))
102 {
103 assert (callback);
104 }
105
106 void addTargetToStop (EditItemID targetID)
107 {
108 if (contains_v (targetIDs, targetID))
109 return;
110
111 targetIDs.push_back (targetID);
112 timer.startTimerHz (10);
113 }
114
115 bool isQueued (EditItemID targetID) const
116 {
117 return contains_v (targetIDs, targetID);
118 }
119
120private:
121 LambdaTimer timer { [this] { checkForTargetsThatHaveFinished(); } };
122 std::vector<EditItemID> targetIDs;
123 std::function<HasFinished (EditItemID)> callback;
124
125 void checkForTargetsThatHaveFinished()
126 {
127 std::vector<EditItemID> targetsToErase;
128
129 for (auto targetID : targetIDs)
130 if (callback (targetID) == HasFinished::yes)
131 targetsToErase.push_back (targetID);
132
133 targetIDs.erase (std::remove_if (targetIDs.begin(), targetIDs.end(),
134 [&] (auto tID) { return contains_v (targetsToErase, tID); }),
135 targetIDs.end());
136
137 if (targetIDs.empty())
138 timer.stopTimer();
139 }
140};
141
142
143//==============================================================================
144//==============================================================================
146{
147public:
148 MidiControllerParser (Engine& e) : engine (e) {}
149
150 void processMessage (const juce::MidiMessage& m)
151 {
152 const int channel = m.getChannel();
153
154 if (m.isController())
155 {
156 const int number = m.getControllerNumber();
157 const int value = m.getControllerValue();
158
159 if (number == 98)
160 {
161 lastNRPNFine = value;
162 wasNRPN = true;
163 }
164 else if (number == 99)
165 {
166 lastNRPNCoarse = value;
167 wasNRPN = true;
168 }
169 else if (number == 100)
170 {
171 lastRPNFine = value;
172 wasNRPN = false;
173 }
174 else if (number == 101)
175 {
176 lastRPNCoarse = value;
177 wasNRPN = false;
178 }
179 else if (number == 6)
180 {
181 // data entry msb..
182 if (wasNRPN)
183 lastParamNumber = 0x20000 + (lastNRPNCoarse << 7) + lastNRPNFine;
184 else
185 lastParamNumber = 0x30000 + (lastRPNCoarse << 7) + lastRPNFine;
186
187 if (lastParamNumber != 0)
188 {
189 controllerMoved (lastParamNumber, value, channel);
190 lastParamNumber = 0;
191 }
192 }
193 else if (number == 0x26)
194 {
195 // data entry lsb - ignore this because it doesn't always get sent,
196 // so we can't wait for it..
197 }
198 else
199 {
200 // not sure if this is going to work yet, shove the channel number in the high byte
201 lastParamNumber = 0x10000 + number;
202 controllerMoved (lastParamNumber, value, channel);
203 }
204 }
205 else if (m.isChannelPressure())
206 {
207 lastParamNumber = 0x40000;
208 controllerMoved (lastParamNumber, m.getChannelPressureValue(), channel);
209 }
210 }
211
212private:
213 void controllerMoved (int number, int value, int channel)
214 {
215 {
216 const juce::ScopedLock sl (pendingLock);
217 pendingMessages.add ({ number, channel, value / 127.0f });
218 }
219
221 }
222
223 void handleAsyncUpdate() override
224 {
225 juce::Array<Message> messages;
226
227 {
228 const juce::ScopedLock sl (pendingLock);
229 pendingMessages.swapWith (messages);
230 }
231
232 if (auto pcm = ParameterControlMappings::getCurrentlyFocusedMappings (engine))
233 for (const auto& m : messages)
234 pcm->sendChange (m.controllerID, m.newValue, m.channel);
235 }
236
237 int lastParamNumber = 0;
238 int lastRPNCoarse = 0, lastRPNFine = 0, lastNRPNCoarse = 0, lastNRPNFine = 0;
239 bool wasNRPN = false;
240
241 struct Message
242 {
243 int controllerID;
244 int channel;
245 float newValue;
246 };
247
248 Engine& engine;
249 juce::Array<Message> pendingMessages;
250 juce::CriticalSection pendingLock;
251};
252
253//==============================================================================
255{
257 {
258 lengthInSeconds = e.getPropertyStorage().getProperty (SettingID::retrospectiveRecord, 30);
259 }
260
261 void addMessage (const juce::MidiMessage& m, double adjust)
262 {
263 if (lock.try_lock())
264 {
265 auto cutoffTime = juce::Time::getMillisecondCounterHiRes() * 0.001
266 + adjust - lengthInSeconds;
267
268 if (m.getTimeStamp() > cutoffTime)
269 sequence.add (m);
270
271 // remove all events that are no longer in the time window
272 int unused = 0;
273 for (int i = 0; i < sequence.size() && sequence[i].getTimeStamp() < cutoffTime; i++)
274 unused = i + 1;
275
276 if (unused > 0)
277 sequence.removeRange (0, unused);
278
279 lock.unlock();
280 }
281 }
282
283 juce::MidiMessageSequence takeMidiMessages()
284 {
286
287 {
288 lock.lock();
289
290 for (auto m : sequence)
291 result.addEvent (m);
292
293 sequence.clear();
294
295 lock.unlock();
296 }
297
298 result.updateMatchedPairs();
299
300 juce::SortedSet<int> usedIndexes;
301
302 // remove all unmatched note on / off from the sequence
303 for (int i = 0; i < result.getNumEvents(); ++i)
304 {
305 auto evt = result.getEventPointer (i);
306
307 if (evt->message.isNoteOn() && evt->noteOffObject != nullptr)
308 {
309 usedIndexes.add (i);
310 usedIndexes.add (result.getIndexOfMatchingKeyUp (i));
311 }
312 }
313
314 for (int i = result.getNumEvents(); --i >= 0;)
315 {
316 auto evt = result.getEventPointer (i);
317
318 if (evt->message.isNoteOnOrOff() && ! usedIndexes.contains (i))
319 result.deleteEvent (i, false);
320 }
321
322 result.updateMatchedPairs();
323
324 return result;
325 }
326
328 double lengthInSeconds = 0;
329 RealTimeSpinLock lock;
330};
331
332//==============================================================================
333MidiInputDevice::MidiInputDevice (Engine& e, juce::String deviceType, juce::String deviceName, juce::String deviceIDToUse)
334 : InputDevice (e, std::move (deviceType), std::move (deviceName), std::move (deviceIDToUse))
335{
336 enabled = true;
337 levelMeasurer.setShowMidi (true);
338
339 std::memset (keysDown, 0, sizeof (keysDown));
340 std::memset (keysUp, 0, sizeof (keysUp));
341 std::memset (keyDownVelocities, 0, sizeof (keyDownVelocities));
342
343 keyboardState.addListener (this);
344}
345
346MidiInputDevice::~MidiInputDevice()
347{
348 keyboardState.removeListener (this);
349 notifyListenersOfDeletion();
350}
351
352void MidiInputDevice::setEnabled (bool b)
353{
354 if (b != enabled)
355 {
356 enabled = b;
357 saveProps();
358 engine.getDeviceManager().rescanMidiDeviceList();
359 }
360}
361
362void MidiInputDevice::loadMidiProps (const juce::XmlElement* n)
363{
364 monitorMode = defaultMonitorMode;
365 recordingEnabled = true;
366 mergeRecordings = true;
367 replaceExistingClips = false;
368 recordToNoteAutomation = isMPEDevice();
369 adjustSecs = 0;
370 manualAdjustMs = 0;
371 minimumLengthMs = 0;
372 channelToUse = {};
373 programToUse = 0;
374 bankToUse = 0;
375 overrideNoteVels = false;
376 disallowedChannels.clear();
377
378 if (n != nullptr)
379 {
380 if (! isTrackDevice())
381 enabled = n->getBoolAttribute ("enabled", enabled);
382
383 monitorMode = magic_enum::enum_cast<MonitorMode> (n->getStringAttribute ("monitorMode").toStdString()).value_or (monitorMode);
384 recordingEnabled = n->getBoolAttribute ("recEnabled", recordingEnabled);
385 mergeRecordings = n->getBoolAttribute ("mergeRecordings", mergeRecordings);
386 replaceExistingClips = n->getBoolAttribute ("replaceExisting", replaceExistingClips);
387 recordToNoteAutomation = n->getBoolAttribute ("recordToNoteAutomation", recordToNoteAutomation) || isMPEDevice();
388 quantisation.setType (n->getStringAttribute ("quantisation"));
389 channelToUse = MidiChannel::fromChannelOrZero (n->getIntAttribute ("channel", channelToUse.getChannelNumber()));
390 programToUse = n->getIntAttribute ("program", programToUse);
391 bankToUse = n->getIntAttribute ("bank", bankToUse);
392 overrideNoteVels = n->getBoolAttribute ("useFullVelocity", overrideNoteVels);
393 manualAdjustMs = n->getDoubleAttribute ("manualAdjustMs", manualAdjustMs);
394 minimumLengthMs = n->getDoubleAttribute ("minimumLengthMs", minimumLengthMs);
395 disallowedChannels.parseString (n->getStringAttribute ("disallowedChannels", disallowedChannels.toString (2)), 2);
396 noteFilterRange.set (n->getIntAttribute ("noteStart", 0),
397 n->getIntAttribute ("noteEnd", 128));
398
399 connectionStateChanged();
400 }
401
402 if (minimumLengthMs > 0 && noteDispatcher == nullptr)
403 noteDispatcher = std::make_unique<NoteDispatcher> (*this);
404 else if (minimumLengthMs <= 0)
405 noteDispatcher = nullptr;
406
407 lastNoteOns.resize (minimumLengthMs > 0 ? 128 * 16 : 0);
408}
409
410void MidiInputDevice::saveMidiProps (juce::XmlElement& n)
411{
412 n.setAttribute ("enabled", enabled);
413 n.setAttribute ("monitorMode", std::string (magic_enum::enum_name (monitorMode)));
414 n.setAttribute ("recEnabled", recordingEnabled);
415 n.setAttribute ("mergeRecordings", mergeRecordings);
416 n.setAttribute ("replaceExisting", replaceExistingClips);
417 n.setAttribute ("recordToNoteAutomation", recordToNoteAutomation);
418 n.setAttribute ("quantisation", quantisation.getType (false));
419 n.setAttribute ("channel", channelToUse.getChannelNumber());
420 n.setAttribute ("program", programToUse);
421 n.setAttribute ("bank", bankToUse);
422 n.setAttribute ("useFullVelocity", overrideNoteVels);
423 n.setAttribute ("manualAdjustMs", manualAdjustMs);
424 n.setAttribute ("minimumLengthMs", minimumLengthMs);
425
426 if (! disallowedChannels.isZero())
427 n.setAttribute ("disallowedChannels", disallowedChannels.toString (2));
428
429 if (! noteFilterRange.isAllNotes())
430 {
431 n.setAttribute ("noteStart", noteFilterRange.startNote);
432 n.setAttribute ("noteEnd", noteFilterRange.endNote);
433 }
434}
435
436juce::Array<AudioTrack*> MidiInputDevice::getDestinationTracks()
437{
438 if (auto edit = engine.getUIBehaviour().getLastFocusedEdit())
439 if (auto in = edit->getCurrentInstanceForInputDevice (this))
440 return getTargetTracks (*in);
441
442 return {};
443}
444
445void MidiInputDevice::setChannelAllowed (int midiChannel, bool allowed)
446{
447 if (allowed != isChannelAllowed (midiChannel))
448 {
449 disallowedChannels.setBit (midiChannel - 1, ! allowed);
450 changed();
451 saveProps();
452 }
453}
454
455void MidiInputDevice::setNoteFilterRange (NoteFilterRange newRange)
456{
457 if (noteFilterRange.startNote != newRange.startNote
458 || noteFilterRange.endNote != newRange.endNote)
459 {
460 noteFilterRange = newRange;
461 changed();
462 saveProps();
463 }
464}
465
466void MidiInputDevice::setChannelToUse (int newChan)
467{
468 auto chan = MidiChannel::fromChannelOrZero (newChan);
469
470 if (channelToUse != chan)
471 {
472 channelToUse = chan;
473 changed();
474 saveProps();
475 }
476}
477
478void MidiInputDevice::setProgramToUse (int prog)
479{
480 programToUse = juce::jlimit (0, 128, prog);
481 changed();
482}
483
484void MidiInputDevice::setBankToUse (int bank)
485{
486 bankToUse = bank;
487}
488
489void MidiInputDevice::setOverridingNoteVelocities (bool b)
490{
491 if (overrideNoteVels != b)
492 {
493 overrideNoteVels = b;
494 changed();
495 saveProps();
496 }
497}
498
499void MidiInputDevice::setManualAdjustmentMs (double ms)
500{
501 if (manualAdjustMs != ms)
502 {
503 manualAdjustMs = ms;
504 changed();
505 saveProps();
506 }
507}
508
509void MidiInputDevice::setMinimumLengthMs (double ms)
510{
511 if (minimumLengthMs != ms)
512 {
513 minimumLengthMs = ms;
514 changed();
515 saveProps();
516
517 if (minimumLengthMs > 0 && noteDispatcher == nullptr)
518 noteDispatcher = std::make_unique<NoteDispatcher> (*this);
519 else if (minimumLengthMs <= 0)
520 noteDispatcher = nullptr;
521
522 lastNoteOns.resize (minimumLengthMs > 0 ? 128 * 16 : 0);
523 }
524}
525
527{
528 return getName().contains ("Seaboard");
529}
530
531//==============================================================================
532void MidiInputDevice::handleNoteOn (juce::MidiKeyboardState*, int /*midiChannel*/, int midiNoteNumber, float velocity)
533{
534 if (eventReceivedFromDevice)
535 return;
536
537 handleIncomingMidiMessage (juce::MidiMessage::noteOn (std::max (1, channelToUse.getChannelNumber()), midiNoteNumber, velocity));
538}
539
540void MidiInputDevice::handleNoteOff (juce::MidiKeyboardState*, int /*midiChannel*/, int midiNoteNumber, float)
541{
542 if (eventReceivedFromDevice)
543 return;
544
545 handleIncomingMidiMessage (juce::MidiMessage::noteOff (std::max (1, channelToUse.getChannelNumber()), midiNoteNumber));
546}
547
548void MidiInputDevice::handleIncomingMidiMessage (juce::MidiInput*, const juce::MidiMessage& m)
549{
550 const juce::ScopedValueSetter<bool> svs (eventReceivedFromDevice, true, false);
551 handleIncomingMidiMessage (m);
552 keyboardState.processNextMidiEvent (m);
553}
554
555void MidiInputDevice::updateRetrospectiveBufferLength (double length)
556{
557 if (retrospectiveBuffer != nullptr)
558 retrospectiveBuffer->lengthInSeconds = length;
559}
560
561//==============================================================================
562void MidiInputDevice::addInstance (MidiInputDeviceInstanceBase* i) { const juce::ScopedLock sl (instanceLock); instances.addIfNotAlreadyThere (i); }
563void MidiInputDevice::removeInstance (MidiInputDeviceInstanceBase* i) { const juce::ScopedLock sl (instanceLock); instances.removeAllInstancesOf (i); }
564
565void MidiInputDevice::connectionStateChanged()
566{
567 if (isTrackDevice() && (! enabled))
568 return;
569
570 if (programToUse > 0 && channelToUse.isValid())
571 {
572 int bankID;
573
574 if (auto destTrack = getDestinationTracks().getFirst())
575 bankID = destTrack->getIdForBank (bankToUse);
576 else
577 bankID = engine.getMidiProgramManager().getBankID (0, bankToUse);
578
579 auto chan = channelToUse.getChannelNumber();
580 handleIncomingMidiMessage (juce::MidiMessage::controllerEvent (chan, 0x00, MidiControllerEvent::bankIDToCoarse (bankID)));
581 handleIncomingMidiMessage (juce::MidiMessage::controllerEvent (chan, 0x20, MidiControllerEvent::bankIDToFine (bankID)));
582 handleIncomingMidiMessage (juce::MidiMessage::programChange (chan, programToUse - 1));
583 }
584}
585
586//==============================================================================
587void MidiInputDevice::sendNoteOnToMidiKeyListeners (juce::MidiMessage& message)
588{
589 if (message.isNoteOn())
590 {
591 if (overrideNoteVels)
592 message.setVelocity (1.0f);
593
594 levelMeasurer.processMidiLevel (message.getFloatVelocity());
595
596 const int noteNum = message.getNoteNumber();
597
598 {
599 juce::ScopedLock sl (noteLock);
600 keysDown[noteNum] = true;
601 keyDownVelocities[noteNum] = message.getVelocity();
602 }
603
604 startTimer (50);
605 }
606 else if (message.isNoteOff())
607 {
608 const int noteNum = message.getNoteNumber();
609
610 {
611 juce::ScopedLock sl (noteLock);
612 keysUp[noteNum] = true;
613 }
614
615 startTimer (25);
616 }
617}
618
619void MidiInputDevice::timerCallback()
620{
621 stopTimer();
622
623 juce::Array<int> down, vels, up;
624
625 bool keysDownCopy[128], keysUpCopy[128];
626 uint8_t keyDownVelocitiesCopy[128];
627
628 {
629 juce::ScopedLock sl (noteLock);
630
631 memcpy (keysUpCopy, keysUp, sizeof (keysUp));
632 memcpy (keysDownCopy, keysDown, sizeof (keysDown));
633 memcpy (keyDownVelocitiesCopy, keyDownVelocities, sizeof (keyDownVelocities));
634
635 std::memset (keysDown, 0, sizeof (keysDown));
636 std::memset (keysUp, 0, sizeof (keysUp));
637 std::memset (keyDownVelocities, 0, sizeof (keyDownVelocities));
638 }
639
640 for (int i = 0; i < 128; ++i)
641 {
642 if (keysDownCopy[i])
643 {
644 down.add (i);
645 vels.add (keyDownVelocitiesCopy[i]);
646 }
647 if (keysUpCopy[i])
648 {
649 up.add (i);
650 }
651 }
652
653 if (down.size() > 0 || up.size() > 0)
654 for (auto t : getDestinationTracks())
655 midiKeyChangeDispatcher->listeners.call (&MidiKeyChangeDispatcher::Listener::midiKeyStateChanged, t, down, vels, up);
656}
657
658//==============================================================================
659static bool trackContainsClipWithName (const AudioTrack& track, const juce::String& name)
660{
661 for (auto& c : track.getClips())
662 if (c->getName().equalsIgnoreCase (name))
663 return true;
664
665 return false;
666}
667
668static juce::String getNameForNewClip (AudioTrack& track)
669{
670 for (int index = 1; ; ++index)
671 {
672 auto clipName = track.getName() + " " + TRANS("Recording") + " " + juce::String (index);
673
674 if (! trackContainsClipWithName (track, clipName))
675 return clipName;
676 }
677}
678
679static juce::String getNameForNewClip (ClipOwner& owner)
680{
681 if (auto slot = dynamic_cast<ClipSlot*> (&owner))
682 if (auto at = dynamic_cast<AudioTrack*> (&slot->track))
683 return getNameForNewClip (*at);
684
685 if (auto at = dynamic_cast<AudioTrack*> (&owner))
686 return getNameForNewClip (*at);
687
688 return {};
689}
690
691
692Clip* MidiInputDevice::addMidiAsTransaction (Edit& ed, EditItemID targetID,
693 Clip* takeClip, juce::MidiMessageSequence ms,
694 TimeRange position, MergeMode merge, MidiChannel midiChannel)
695{
697 Clip* createdClip = nullptr;
698 auto track = findAudioTrackForID (ed, targetID);
699 auto clipSlot = track != nullptr ? nullptr : findClipSlotForID (ed, targetID);
700 auto clipOwner = track != nullptr ? static_cast<ClipOwner*> (track)
701 : static_cast<ClipOwner*> (clipSlot);
702
703 if (! clipOwner)
704 {
706 return nullptr;
707 }
708
709 quantisation.applyQuantisationToSequence (ms, ed, position.getStart());
710
711 bool needToAddClip = true;
712 const auto automationType = recordToNoteAutomation ? MidiList::NoteAutomationType::expression
714
715 if (track)
716 if ((merge == MergeMode::optional && mergeRecordings) || merge == MergeMode::always)
717 needToAddClip = ! track->mergeInMidiSequence (ms, position.getStart(), nullptr, automationType);
718
719 if (takeClip != nullptr)
720 {
721 if (auto midiClip = dynamic_cast<MidiClip*> (takeClip))
722 {
723 ms.addTimeToMessages (midiClip->getPosition().getStart().inSeconds());
725 midiClip->addTake (ms, automationType);
726
727 midiClip->setMidiChannel (midiChannel);
728
729 if (programToUse > 0)
730 midiClip->getSequence().addControllerEvent ({}, MidiControllerEvent::programChangeType,
731 (programToUse - 1) << 7, &ed.getUndoManager());
732 }
733 }
734 else if (needToAddClip)
735 {
737
738 if ((replaceExistingClips && merge == MergeMode::optional)
739 || clipSlot)
740 {
741 if (track)
742 track->deleteRegion (position, nullptr);
743 else
744 clipSlot->setClip (nullptr);
745 }
746
747 if (auto mc = insertMIDIClip (*clipOwner, getNameForNewClip (*clipOwner), position))
748 {
749 if (track)
750 {
751 track->mergeInMidiSequence (std::move (ms), mc->getPosition().getStart(), mc.get(), automationType);
752 mc->setLength (position.getLength(), true); // Reset the length here as it migh thave been changed by mergeInMidiSequence above
753 }
754 else if (clipSlot)
755 {
756 mergeInMidiSequence (*mc, std::move (ms), toDuration (mc->getPosition().getStart()), automationType);
757 }
758
759 if (recordToNoteAutomation)
760 mc->setMPEMode (true);
761
762 mc->setMidiChannel (midiChannel);
763
764 if (programToUse > 0)
765 mc->getSequence().addControllerEvent ({}, MidiControllerEvent::programChangeType,
766 (programToUse - 1) << 7, &ed.getUndoManager());
767
768 createdClip = mc.get();
769 }
770 else
771 {
773 }
774 }
775
776 return createdClip;
777}
778
779//==============================================================================
781{
782public:
784 : InputDeviceInstance (d, c)
785 {
786 getMidiInput().addInstance (this);
787 }
788
790 {
791 getMidiInput().removeInstance (this);
792 }
793
794 bool isRecordingActive() const override
795 {
796 return getMidiInput().recordingEnabled && InputDeviceInstance::isRecordingActive();
797 }
798
799 bool isRecordingActive (EditItemID targetID) const override
800 {
801 return getMidiInput().recordingEnabled && InputDeviceInstance::isRecordingActive (targetID);
802 }
803
804 bool isRecordingQueuedToStop (EditItemID targetID) override
805 {
806 return getRecordStopper().isQueued (targetID);
807 }
808
809 bool shouldTrackContentsBeMuted (const Track& t) override
810 {
811 return getContextForID (t.itemID) != nullptr
812 && ! getMidiInput().mergeRecordings;
813 }
814
815 virtual void handleMMCMessage (const juce::MidiMessage&) {}
816 virtual bool handleTimecodeMessage (const juce::MidiMessage&) { return false; }
817
819 {
820 const std::shared_lock sl (contextLock);
821
822 if (auto rc = getContextForID (targetID))
823 return rc->liveNotes;
824
825 return {};
826 }
827
829 {
830 public:
831 MidiRecordingContext (EditPlaybackContext& epc, EditItemID target, TimeRange punchRange_)
832 : RecordingContext (target),
833 scopedActiveRecordingDevice (epc),
834 punchRange (punchRange_)
835 {
837 liveNotes->reset (100);
838 }
839
840 detail::ScopedActiveRecordingDevice scopedActiveRecordingDevice;
841 const TimeRange punchRange;
843 TimePosition unloopedStopTime;
844
846
847 std::function<void (tl::expected<Clip::Array, juce::String>)> stopCallback;
848 StopRecordingParameters stopParams;
849 };
850
852 {
853 TRACKTION_ASSERT_MESSAGE_THREAD
855
856 if (params.targets.empty())
857 for (auto dest : destinations)
858 if (dest->recordEnabled)
859 params.targets.push_back (dest->targetID);
860
861 for (auto targetID : params.targets)
862 {
864 targetID,
865 params.punchRange);
866 results.emplace_back (std::move (recContext));
867 }
868
869 return results;
870 }
871
873 {
874 TRACKTION_ASSERT_MESSAGE_THREAD
875 bool hasAddedContexts = false;
876
877 for (auto& recContext : newContexts)
878 {
879 if (auto midiContext = dynamic_cast<MidiRecordingContext*> (recContext.get()))
880 {
881 const auto targetID = midiContext->targetID;
882
883 {
884 const std::unique_lock sl (contextLock);
885 recordingContexts.push_back (std::unique_ptr<MidiRecordingContext> (midiContext));
886 }
887
888 hasAddedContexts = true;
889 recContext.release();
890 context.transport.callRecordingAboutToStartListeners (*this, targetID);
891 }
892 }
893
894 if (hasAddedContexts && ! edit.getTransport().isPlaying())
895 edit.getTransport().play (false);
896
897 // Remove now empty contents and return the rest
898 return std::move (erase_if_null (newContexts));
899 }
900
901 tl::expected<Clip::Array, juce::String> stopRecording (StopRecordingParameters params) override
902 {
903 TRACKTION_ASSERT_MESSAGE_THREAD
904 // Reserve to avoid allocating whilst
905 const auto numContextsRecording = [this]
906 {
907 const std::shared_lock sl (contextLock);
908 return recordingContexts.size();
909 }();
910
912 contextsToStop.reserve (numContextsRecording);
913
914 // Stop the relevant contexts
915 {
916 const std::unique_lock sl (contextLock);
917
918 for (auto& recContext : recordingContexts)
919 {
920 if (! params.targetsToStop.empty())
921 if (! contains_v (params.targetsToStop, recContext->targetID))
922 continue;
923
924 recContext->unloopedStopTime = params.unloopedTimeToEndRecording;
925 contextsToStop.push_back (std::move (recContext));
926 }
927
928 erase_if_null (recordingContexts);
929 assert ((recordingContexts.size() + contextsToStop.size()) == numContextsRecording);
930 }
931
932 // Now apply those stop contexts whilst not holding the lock
933 Clip::Array clips;
934
935 for (auto& recContext : contextsToStop)
936 {
937 const auto targetID = recContext->targetID;
938 auto stopCallback = std::move (recContext->stopCallback);
939 context.transport.callRecordingAboutToStopListeners (*this, targetID);
940 auto contextClips = applyRecording (std::move (recContext),
942 params.isLooping, params.markedRange,
943 params.discardRecordings);
944 context.transport.callRecordingFinishedListeners (*this, targetID, contextClips);
945
946 if (stopCallback)
947 stopCallback (contextClips);
948
949 clips.addArray (std::move (contextClips));
950 }
951
952 return clips;
953 }
954
956 std::function<void (tl::expected<Clip::Array, juce::String>)> callback) override
957 {
958 TRACKTION_ASSERT_MESSAGE_THREAD
959 // Reserve to avoid allocating whilst the lock is held
960 const auto getNumContextsRecording = [this]
961 {
962 const std::shared_lock sl (contextLock);
963 return recordingContexts.size();
964 };
965
966 if (params.targetsToStop.empty())
967 {
968 params.targetsToStop.reserve (getNumContextsRecording());
969
970 const std::shared_lock sl (contextLock);
971
972 for (auto& recContext : recordingContexts)
973 params.targetsToStop.push_back (recContext->targetID);
974 }
975
976 // Set the punch out time for the contexts
977 {
978 const std::unique_lock sl (contextLock);
979
980 for (auto& recContext : recordingContexts)
981 {
982 if (! contains_v (params.targetsToStop, recContext->targetID))
983 continue;
984
985 recContext->unloopedStopTime = params.unloopedTimeToEndRecording;
986
987 // Unlock whilst doing potentially allocating ops to avoid priority inversion
988 {
989 contextLock.unlock();
990 recContext->stopCallback = callback;
991 recContext->stopParams = params;
992 recContext->stopParams.targetsToStop = { recContext->targetID };
993 contextLock.lock();
994 }
995 }
996 }
997
998 // Add the rec context to a timer list to poll if the recording can be stopped
999 for (auto targetID : params.targetsToStop)
1000 getRecordStopper().addTargetToStop (targetID);
1001 }
1002
1004 {
1005 const std::shared_lock sl (contextLock);
1006
1007 for (auto& recContext : recordingContexts)
1008 if (recContext->targetID == targetID)
1009 return recContext->punchRange.getStart();
1010
1012 }
1013
1014 bool isRecording (EditItemID targetID) override
1015 {
1016 return getContextForID (targetID) != nullptr;
1017 }
1018
1019 bool isRecording() override
1020 {
1021 const std::shared_lock sl (contextLock);
1022 return ! recordingContexts.empty();
1023 }
1024
1025 bool handleIncomingMidiMessage (const juce::MidiMessage& message)
1026 {
1027 {
1028 juce::ScopedLock sl (activeNotesLock);
1029
1030 if (message.isNoteOn())
1031 activeNotes.startNote (message.getChannel(), message.getNoteNumber());
1032 else if (message.isNoteOff())
1033 activeNotes.clearNote (message.getChannel(), message.getNoteNumber());
1034 }
1035
1036 const bool recording = isRecording();
1037
1038 if (recording)
1039 {
1040 auto m1 = juce::MidiMessage (message, context.globalStreamTimeToEditTime (message.getTimeStamp()).inSeconds());
1041 auto m2 = juce::MidiMessage (message, context.globalStreamTimeToEditTimeUnlooped (message.getTimeStamp()).inSeconds());
1042
1043 for (auto& recContext : recordingContexts)
1044 recContext->liveNotes->push (m1);
1045
1046 const std::shared_lock sl (contextLock);
1047
1048 for (auto& recContext : recordingContexts)
1049 recContext->recorded.addEvent (m2);
1050 }
1051
1052 juce::ScopedLock sl (consumerLock);
1053
1054 for (auto c : consumers)
1055 c->handleIncomingMidiMessage (message);
1056
1057 return recording || consumers.size() > 0;
1058 }
1059
1060 MidiChannel applyChannel (juce::MidiMessageSequence& sequence, MidiChannel channelToApply)
1061 {
1062 if (! channelToApply.isValid())
1063 {
1064 for (int i = 0; i < sequence.getNumEvents(); ++i)
1065 {
1066 auto chan = MidiChannel::fromChannelOrZero (sequence.getEventPointer (i)->message.getChannel());
1067
1068 if (chan.isValid())
1069 {
1070 channelToApply = chan;
1071 break;
1072 }
1073 }
1074 }
1075
1076 if (channelToApply.isValid())
1077 for (int i = sequence.getNumEvents(); --i >= 0;)
1078 sequence.getEventPointer(i)->message.setChannel (channelToApply.getChannelNumber());
1079
1080 return channelToApply;
1081 }
1082
1083 static void applyTimeAdjustment (juce::MidiMessageSequence& sequence, double adjustmentMs)
1084 {
1085 if (adjustmentMs != 0)
1086 sequence.addTimeToMessages (adjustmentMs * 0.001);
1087 }
1088
1089 Clip::Array applyRecording (std::unique_ptr<MidiRecordingContext> recContext,
1090 TimePosition unloopedEndTime,
1091 bool isLooping, TimeRange loopRange,
1092 bool discardRecordings)
1093 {
1095 if (! recContext || discardRecordings)
1096 {
1097 for (juce::ScopedLock sl (consumerLock); auto c : consumers)
1098 c->discardRecordings (recContext ? recContext->targetID : EditItemID());
1099
1100 return {};
1101 }
1102
1103 if (recContext->recorded.getNumEvents() == 0)
1104 return {};
1105
1106 auto track = findAudioTrackForID (edit, recContext->targetID);
1107 auto clipSlot = track ? nullptr : findClipSlotForID (edit, recContext->targetID);
1108
1109 if (! track && ! clipSlot)
1110 return {};
1111
1112 // Never add takes for slot recordings
1113 if (clipSlot)
1114 isLooping = false;
1115
1116 auto clipOwner = track != nullptr ? static_cast<ClipOwner*> (track)
1117 : static_cast<ClipOwner*> (clipSlot);
1118 Clip::Array createdClips;
1119 auto& mi = getMidiInput();
1120 auto& recorded = recContext->recorded;
1121 const bool wasPunchRecording = clipSlot ? false : edit.recordingPunchInOut;
1122
1123 recorded.updateMatchedPairs();
1124 auto channelToApply = mi.recordToNoteAutomation ? mi.getChannelToUse()
1125 : applyChannel (recorded, mi.getChannelToUse());
1126 auto timeAdjustMs = mi.getManualAdjustmentMs();
1127
1128 if (context.getNodePlayHead() != nullptr)
1129 timeAdjustMs -= 1000.0 * tracktion::graph::sampleToTime (context.getLatencySamples(), edit.engine.getDeviceManager().getSampleRate());
1130
1131 applyTimeAdjustment (recorded, timeAdjustMs);
1132
1133 TimeRange recordedRange (recContext->punchRange.getStart(), unloopedEndTime);
1134 auto recordingStart = recordedRange.getStart();
1135 auto recordingEnd = recordedRange.getEnd();
1136
1137 const bool createTakes = mi.recordingEnabled && ! (mi.mergeRecordings || mi.replaceExistingClips);
1138
1139 if (isLooping && recordingEnd > loopRange.getEnd())
1140 {
1141 juce::MidiMessageSequence replaceSequence;
1142 const auto loopLen = loopRange.getLength();
1143 const auto maxNumLoops = 2 + (int) ((recordingEnd - recordingStart) / loopLen);
1144
1145 Clip* takeClip = nullptr;
1146
1147 for (int loopNum = 0; loopNum < maxNumLoops; ++loopNum)
1148 {
1149 juce::MidiMessageSequence loopSequence;
1150 const auto thisLoopStart = loopRange.getStart() + loopLen * loopNum;
1151 const auto thisLoopEnd = (thisLoopStart + loopLen).inSeconds();
1152
1153 for (int i = 0; i < recorded.getNumEvents(); ++i)
1154 {
1155 auto& m = recorded.getEventPointer (i)->message;
1156
1157 if (m.isNoteOn())
1158 {
1159 double s = m.getTimeStamp();
1160 double e = s;
1161
1162 if (auto noteOff = recorded.getEventPointer (i)->noteOffObject)
1163 e = noteOff->message.getTimeStamp();
1164
1165 if (e > thisLoopStart.inSeconds() && s < thisLoopEnd)
1166 {
1167 if (s < thisLoopStart.inSeconds())
1168 s = 0.0;
1169 else
1170 s = std::fmod (s - loopRange.getStart().inSeconds(), loopLen.inSeconds());
1171
1172 if (e > thisLoopEnd)
1173 e = loopLen.inSeconds();
1174 else
1175 e = std::fmod (e - loopRange.getStart().inSeconds(), loopLen.inSeconds());
1176
1177 loopSequence.addEvent (juce::MidiMessage (m, s));
1178 loopSequence.addEvent (juce::MidiMessage (juce::MidiMessage::noteOff (m.getChannel(),
1179 m.getNoteNumber()), e));
1180 }
1181 }
1182 else if (! m.isNoteOff())
1183 {
1184 const double t = m.getTimeStamp();
1185
1186 if (t >= thisLoopStart.inSeconds() && t < thisLoopEnd)
1187 loopSequence.addEvent (juce::MidiMessage (m, std::fmod (t - loopRange.getStart().inSeconds(), loopLen.inSeconds())));
1188 }
1189 }
1190
1191 if (loopSequence.getNumEvents() > 0)
1192 {
1193 loopSequence.updateMatchedPairs();
1194
1195 if (createTakes)
1196 {
1197 if (auto clip = mi.addMidiAsTransaction (edit, clipOwner->getClipOwnerID(), takeClip,
1198 std::move (loopSequence), loopRange,
1199 MidiInputDevice::MergeMode::optional,
1200 channelToApply))
1201 {
1202 takeClip = clip;
1203 createdClips.add (clip);
1204 }
1205 }
1206 else if (mi.replaceExistingClips)
1207 {
1208 replaceSequence = std::move (loopSequence);
1209 }
1210 else
1211 {
1212 if (auto clip = mi.addMidiAsTransaction (edit, clipOwner->getClipOwnerID(), nullptr,
1213 std::move (loopSequence), loopRange,
1214 MidiInputDevice::MergeMode::always,
1215 channelToApply))
1216 {
1217 createdClips.add (clip);
1218 }
1219 }
1220 }
1221 }
1222
1223 if (mi.replaceExistingClips && replaceSequence.getNumEvents() > 0)
1224 {
1225 if (auto clip = mi.addMidiAsTransaction (edit, clipOwner->getClipOwnerID(), nullptr,
1226 std::move (replaceSequence), loopRange,
1227 MidiInputDevice::MergeMode::optional,
1228 channelToApply))
1229 {
1230 createdClips.add (clip);
1231 }
1232 }
1233 }
1234 else
1235 {
1236 auto startPos = recordingStart;
1237 auto endPos = recordingEnd;
1238 auto maxEndPos = endPos + 0.5s;
1239
1240 if (wasPunchRecording)
1241 {
1242 if (startPos < loopRange.getEnd())
1243 {
1244 // if we didn't get as far as the punch-in
1245 if (endPos <= loopRange.getStart())
1246 return createdClips;
1247
1248 startPos = std::max (startPos, loopRange.getStart());
1249 endPos = juce::jlimit (startPos + 0.1s, loopRange.getEnd(), endPos);
1250 maxEndPos = endPos;
1251 }
1252 else if (edit.getNumCountInBeats() > 0)
1253 {
1254 startPos = std::max (startPos, loopRange.getStart());
1255 }
1256 }
1257
1258 juce::Array<int> eventsToDelete;
1259 juce::Array<juce::MidiMessage> noteOffMessagesToAdd, mpeMessagesToAddAtStartPos;
1260
1261 const auto ensureNoteOffIsInsideClip = [&noteOffMessagesToAdd, endPos] (juce::MidiMessageSequence::MidiEventHolder& m)
1262 {
1263 jassert (m.message.isNoteOn());
1264
1265 if (m.noteOffObject == nullptr)
1266 noteOffMessagesToAdd.add (juce::MidiMessage (juce::MidiMessage::noteOff (m.message.getChannel(),
1267 m.message.getNoteNumber()), endPos.inSeconds()));
1268
1269 else if (m.noteOffObject->message.getTimeStamp() > endPos.inSeconds())
1270 m.noteOffObject->message.setTimeStamp (endPos.inSeconds());
1271 };
1272
1273 const auto isNoteOnThatEndsAfterClipStart = [startPos] (juce::MidiMessageSequence::MidiEventHolder& m)
1274 {
1275 return m.message.isNoteOn() && (m.noteOffObject == nullptr || m.noteOffObject->message.getTimeStamp() > startPos.inSeconds());
1276 };
1277
1278 const auto isOutsideClipAndNotNoteOff = [startPos, maxEndPos] (const juce::MidiMessage& m)
1279 {
1280 return (m.getTimeStamp() < startPos.inSeconds() || m.getTimeStamp() > maxEndPos.inSeconds()) && ! m.isNoteOff();
1281 };
1282
1283 if (mi.recordToNoteAutomation)
1284 {
1285 auto clipStartIndex = recorded.getNextIndexAtTime (startPos.inSeconds());
1286
1287 for (int i = recorded.getNumEvents(); --i >= 0;)
1288 {
1289 auto& m = *recorded.getEventPointer (i);
1290
1291 if (m.message.getTimeStamp() < startPos.inSeconds() && isNoteOnThatEndsAfterClipStart (m))
1292 {
1293 MPEStartTrimmer::reconstructExpression (mpeMessagesToAddAtStartPos, recorded, clipStartIndex, m.message.getChannel());
1294
1295 ensureNoteOffIsInsideClip (m);
1296 eventsToDelete.add (i); // Remove original noteOn, findInitialNoteExpression makes new one in correct order
1297 m.noteOffObject = nullptr; // Prevent deletion of original note off, it will be paird with the new note-on later
1298 }
1299 else if (isOutsideClipAndNotNoteOff (m.message))
1300 {
1301 eventsToDelete.add (i);
1302 }
1303 else if (m.message.getTimeStamp() < endPos.inSeconds() && m.message.isNoteOn())
1304 {
1305 ensureNoteOffIsInsideClip (m);
1306 }
1307 }
1308 }
1309 else
1310 {
1311 for (int i = recorded.getNumEvents(); --i >= 0;)
1312 {
1313 auto& m = *recorded.getEventPointer (i);
1314
1315 if (m.message.getTimeStamp() < startPos.inSeconds() && isNoteOnThatEndsAfterClipStart (m))
1316 m.message.setTimeStamp (startPos.inSeconds());
1317
1318 if (isOutsideClipAndNotNoteOff (m.message))
1319 eventsToDelete.add (i);
1320 else if (m.message.getTimeStamp() < endPos.inSeconds() && m.message.isNoteOn())
1321 ensureNoteOffIsInsideClip (m);
1322 }
1323 }
1324
1325 if (! eventsToDelete.isEmpty())
1326 {
1327 // N.B. eventsToDelete is in reverse order so iterate forwards when deleting
1328 for (int index : eventsToDelete)
1329 recorded.deleteEvent (index, true);
1330 }
1331
1332 if (! noteOffMessagesToAdd.isEmpty())
1333 {
1334 for (const auto& m : noteOffMessagesToAdd)
1335 recorded.addEvent (m);
1336 }
1337
1338 if (! mpeMessagesToAddAtStartPos.isEmpty())
1339 {
1340 for (const auto& m : mpeMessagesToAddAtStartPos)
1341 recorded.addEvent (m, startPos.inSeconds());
1342 }
1343
1344 recorded.sort();
1345 recorded.updateMatchedPairs();
1346 recorded.addTimeToMessages (-startPos.inSeconds());
1347
1348 if (recorded.getNumEvents() > 0)
1349 {
1350 if (auto clip = mi.addMidiAsTransaction (edit, clipOwner->getClipOwnerID(), nullptr,
1351 std::move (recorded), { startPos, endPos },
1352 MidiInputDevice::MergeMode::optional,
1353 channelToApply))
1354 {
1355 createdClips.add (clip);
1356 }
1357 }
1358 }
1359
1360 for (juce::ScopedLock sl (consumerLock); auto c : consumers)
1361 c->discardRecordings (recContext->targetID);
1362
1363 return createdClips;
1364 }
1365
1367 {
1369
1370 juce::Array<Clip*> clips;
1371
1372 auto& mi = getMidiInput();
1373 auto retrospective = mi.getRetrospectiveMidiBuffer();
1374
1375 if (retrospective == nullptr)
1376 return {};
1377
1378 for (auto track : getTargetTracks (*this))
1379 {
1380 if (armedOnly && ! isRecordingActive (track->itemID))
1381 continue;
1382
1383 auto sequence = retrospective->takeMidiMessages();
1384
1385 if (sequence.getNumEvents() == 0)
1386 return {};
1387
1388 sequence.updateMatchedPairs();
1389 auto channelToApply = mi.recordToNoteAutomation ? mi.getChannelToUse()
1390 : applyChannel (sequence, mi.getChannelToUse());
1391 auto timeAdjustMs = mi.getManualAdjustmentMs();
1392
1393 if (context.getNodePlayHead() != nullptr)
1394 timeAdjustMs -= 1000.0 * tracktion::graph::sampleToTime (context.getLatencySamples(), edit.engine.getDeviceManager().getSampleRate());
1395
1396 applyTimeAdjustment (sequence, timeAdjustMs);
1397
1398 auto clipStart = juce::Time::getMillisecondCounterHiRes() * 0.001
1399 - retrospective->lengthInSeconds + mi.getAdjustSecs();
1400 sequence.addTimeToMessages (-clipStart);
1401
1402 double start;
1403 double length = retrospective->lengthInSeconds;
1404 double offset = 0;
1405
1406 if (context.isPlaying())
1407 {
1408 start = std::max (TimePosition(), context.getPosition()).inSeconds() - length;
1409 }
1410 else if (lastEditTime >= 0 && pausedTime < 20)
1411 {
1412 start = lastEditTime + pausedTime - length;
1413 lastEditTime = -1;
1414 }
1415 else
1416 {
1417 auto position = context.getPosition();
1418
1419 if (position >= 5s)
1420 start = position.inSeconds() - length;
1421 else
1422 start = std::max (TimePosition(), context.getPosition()).inSeconds();
1423 }
1424
1425 if (sequence.getNumEvents() > 0)
1426 {
1427 auto firstEventTime = std::floor (sequence.getStartTime());
1428 sequence.addTimeToMessages (-firstEventTime);
1429 start += firstEventTime;
1430 length -= firstEventTime;
1431 }
1432
1433 if (start < 0)
1434 {
1435 offset = -start;
1436 start = 0;
1437 length -= offset;
1438 }
1439
1440 if (sequence.getNumEvents() > 0)
1441 {
1442 auto clip = mi.addMidiAsTransaction (edit, track->getClipOwnerID(), nullptr, std::move (sequence),
1443 { TimePosition::fromSeconds (start), TimePosition::fromSeconds (start + length) },
1444 MidiInputDevice::MergeMode::never,
1445 channelToApply);
1446 clip->setOffset (TimeDuration::fromSeconds (offset));
1447 clips.add (clip);
1448
1449 if (auto mc = dynamic_cast<MidiClip*> (clip))
1450 {
1451 if (track->playSlotClips.get())
1452 {
1453 if (auto slot = getFreeSlot (*track))
1454 {
1455 mc->setUsesProxy (false);
1456 mc->setStart (0_tp, false, true);
1457
1458 if (! mc->isLooping ())
1459 mc->setLoopRangeBeats (mc->getEditBeatRange());
1460
1461 mc->removeFromParent();
1462 slot->setClip (mc);
1463 }
1464 }
1465 }
1466 }
1467 }
1468
1469 return clips;
1470 }
1471
1472 void masterTimeUpdate (double time)
1473 {
1474 if (context.isPlaying())
1475 {
1476 pausedTime = 0;
1477 lastEditTime = context.globalStreamTimeToEditTime (time).inSeconds();
1478 }
1479 else
1480 {
1481 pausedTime += edit.engine.getDeviceManager().getBlockSizeMs() / 1000.0;
1482 }
1483 }
1484
1485 MidiInputDevice& getMidiInput() const { return static_cast<MidiInputDevice&> (owner); }
1486
1487 mutable std::shared_mutex contextLock;
1489
1490private:
1491 juce::CriticalSection consumerLock, activeNotesLock;
1492 juce::Array<Consumer*> consumers;
1493 double lastEditTime = -1.0;
1494 double pausedTime = 0;
1495 MidiMessageArray::MPESourceID midiSourceID = MidiMessageArray::createUniqueMPESourceID();
1496 ActiveNoteList activeNotes;
1497 std::unique_ptr<RecordStopper> recordStopper;
1498
1499 void addConsumer (Consumer* c) override { juce::ScopedLock sl (consumerLock); consumers.addIfNotAlreadyThere (c); }
1500 void removeConsumer (Consumer* c) override { juce::ScopedLock sl (consumerLock); consumers.removeAllInstancesOf (c); }
1501
1502 void valueTreeChildRemoved (juce::ValueTree& p, juce::ValueTree& c, int index) override
1503 {
1504 if (p == state && c.hasType (IDs::INPUTDEVICEDESTINATION))
1505 injectNoteOffsToTrack();
1506
1507 InputDeviceInstance::valueTreeChildRemoved (p, c, index);
1508 }
1509
1510 void injectNoteOffsToTrack()
1511 {
1512 ActiveNoteList notes;
1513
1514 // The lock isn't great here but it's mostly uncontended and when it is, it's between the MIDI and message threads
1515 // The lock time is also constant so low-risk
1516 {
1517 const juce::ScopedLock sl (activeNotesLock);
1518 notes = activeNotes;
1519 }
1520
1521 for (auto t : getTargetTracks (*this))
1522 {
1523 notes.iterate ([&] (auto channel, auto noteNumber)
1524 {
1525 t->injectLiveMidiMessage (juce::MidiMessage::noteOff (channel, noteNumber), midiSourceID);
1526 });
1527 }
1528 }
1529
1530 MidiRecordingContext* getContextForID (EditItemID targetID) const
1531 {
1532 const std::shared_lock sl (contextLock);
1533
1534 for (auto& recContext : recordingContexts)
1535 if (recContext->targetID == targetID)
1536 return recContext.get();
1537
1538 return nullptr;
1539 }
1540
1541 RecordStopper& getRecordStopper()
1542 {
1543 TRACKTION_ASSERT_MESSAGE_THREAD
1544
1545 if (! recordStopper)
1546 {
1547 recordStopper = std::make_unique<RecordStopper> ([this] (auto targetID)
1548 {
1549 const auto unloopedTimeNow = context.getUnloopedPosition();
1550 const std::shared_lock sl (contextLock);
1551
1552 if (auto recContext = getContextForID (targetID))
1553 {
1554 if (unloopedTimeNow >= recContext->unloopedStopTime)
1555 {
1556 auto stopParams = recContext->stopParams;
1557
1558 // Temp unlock as stopRecording takes a unique lock
1559 contextLock.unlock_shared();
1560 auto res = stopRecording (stopParams);
1561 contextLock.lock_shared();
1562
1563 return RecordStopper::HasFinished::yes;
1564 }
1565
1566 return RecordStopper::HasFinished::no;
1567 }
1568
1569 return RecordStopper::HasFinished::yes;
1570 });
1571 }
1572
1573 return *recordStopper;
1574 }
1575
1576 JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiInputDeviceInstanceBase)
1577};
1578
1579
1580//==============================================================================
1582{
1583 if (m.isActiveSense())
1584 return false;
1585
1586 if (disallowedChannels[m.getChannel() - 1])
1587 return false;
1588
1589 if (m.isNoteOnOrOff())
1590 {
1591 auto note = m.getNoteNumber();
1592
1593 if (note < noteFilterRange.startNote || note >= noteFilterRange.endNote)
1594 return false;
1595 }
1596
1597 if (m.getTimeStamp() == 0 || (! engine.getEngineBehaviour().isMidiDriverUsedForIncommingMessageTiming()))
1598 m.setTimeStamp (juce::Time::getMillisecondCounterHiRes() * 0.001);
1599
1600 m.addToTimeStamp (adjustSecs);
1601
1602 if (! retrospectiveRecordLock && retrospectiveBuffer != nullptr)
1603 retrospectiveBuffer->addMessage (m, adjustSecs);
1604
1605 sendNoteOnToMidiKeyListeners (m);
1606 return true;
1607}
1608
1610{
1611 adjustSecs = time - juce::Time::getMillisecondCounterHiRes() * 0.001;
1612
1613 const juce::ScopedLock sl (instanceLock);
1614
1615 for (auto instance : instances)
1616 instance->masterTimeUpdate (time);
1617}
1618
1619void MidiInputDevice::sendMessageToInstances (const juce::MidiMessage& message)
1620{
1621 bool messageUnused = true;
1622
1623 {
1624 const juce::ScopedLock sl (instanceLock);
1625
1626 for (auto i : instances)
1627 if (i->handleIncomingMidiMessage (message))
1628 messageUnused = false;
1629 }
1630
1631 if (messageUnused && message.isNoteOn())
1632 if (auto& warnOfWasted = engine.getDeviceManager().warnOfWastedMidiMessagesFunction)
1633 warnOfWasted (this);
1634}
1635
1636}} // namespace tracktion { inline namespace engine
assert
T begin(T... args)
void swapWith(OtherArrayType &otherArray) noexcept
bool isEmpty() const noexcept
int removeAllInstancesOf(ParameterType valueToRemove)
int size() const noexcept
void removeRange(int startIndex, int numberToRemove)
void add(const ElementType &newElement)
void clear()
bool addIfNotAlreadyThere(ParameterType newElement)
BigInteger & clear() noexcept
void parseString(StringRef text, int base)
String toString(int base, int minimumNumCharacters=1) const
bool isZero() const noexcept
BigInteger & setBit(int bitNumber)
void startTimer(int intervalInMilliseconds)
void processNextMidiEvent(const MidiMessage &message)
void removeListener(Listener *listener)
MidiEventHolder * addEvent(const MidiMessage &newMessage, double timeAdjustment=0)
void updateMatchedPairs() noexcept
int getIndexOfMatchingKeyUp(int index) const noexcept
void addTimeToMessages(double deltaTime) noexcept
void deleteEvent(int index, bool deleteMatchingNoteUp)
MidiEventHolder * getEventPointer(int index) const noexcept
int getNumEvents() const noexcept
bool isNoteOn(bool returnTrueForVelocity0=false) const noexcept
float getFloatVelocity() const noexcept
int getChannel() const noexcept
void setVelocity(float newVelocity) noexcept
bool isNoteOff(bool returnTrueForNoteOnVelocity0=true) const noexcept
static MidiMessage noteOn(int channel, int noteNumber, float velocity) noexcept
int getNoteNumber() const noexcept
double getTimeStamp() const noexcept
static MidiMessage controllerEvent(int channel, int controllerType, int value) noexcept
static MidiMessage noteOff(int channel, int noteNumber, float velocity) noexcept
uint8 getVelocity() const noexcept
static MidiMessage programChange(int channel, int programNumber) noexcept
void addArray(const ReferenceCountedArray &arrayToAddFrom, int startIndex=0, int numElementsToAdd=-1) noexcept
bool add(const ElementType &newElement) noexcept
bool contains(const ElementType &elementToLookFor) const noexcept
bool contains(StringRef text) const noexcept
static double getMillisecondCounterHiRes() noexcept
void stopTimer() noexcept
void startTimerHz(int timerFrequencyHz) noexcept
void startTimer(int intervalInMilliseconds) noexcept
double getDoubleAttribute(StringRef attributeName, double defaultReturnValue=0.0) const
bool getBoolAttribute(StringRef attributeName, bool defaultReturnValue=false) const
int getIntAttribute(StringRef attributeName, int defaultReturnValue=0) const
const String & getStringAttribute(StringRef attributeName) const noexcept
void setAttribute(const Identifier &attributeName, const String &newValue)
std::function< void(InputDevice *)> warnOfWastedMidiMessagesFunction
If this is set, it will get called (possibly on the midi thread) when incoming messages seem to be un...
int getLatencySamples() const
Returns the overall latency of the currently prepared graph.
TransportControl & getTransport() const noexcept
Returns the TransportControl which is used to stop/stop/position playback and recording.
int getNumCountInBeats() const
Returns the number of beats of the count in.
juce::CachedValue< bool > recordingPunchInOut
Whether recoridng only happens within the in/out markers.
Engine & engine
A reference to the Engine.
virtual bool isMidiDriverUsedForIncommingMessageTiming()
Should return true if the incoming timestamp for MIDI messages should be used.
The Engine is the central class for all tracktion sessions.
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.
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.
virtual bool isRecordingActive() const
Returns true if recording is enabled and the input is connected to any target.
bool discardRecordings
Whether to discard recordings or keep them.
std::vector< EditItemID > targetsToStop
The targets to stop, others will continue allowing you to punch out only specific targets.
Edit & edit
The Edit this instance belongs to.
std::vector< EditItemID > targets
The targets to record to, if this is empty, all armed targets will be added.
EditPlaybackContext & context
The EditPlaybackContext this instance belongs to.
InputDevice & owner
The state of this instance.
DestinationList destinations
The list of assigned destinations.
bool isLooping
Whether to treat the stopped recordings as looped or not.
TimeRange punchRange
The transport time range at which the recording should happen.
TimeRange markedRange
The marked range used for either loop or punch times.
TimePosition unloopedTimeToEndRecording
The TimePosition this recording should be stopped at.
Represents an input device.
void stopRecording(StopRecordingParameters params, std::function< void(tl::expected< Clip::Array, juce::String >)> callback) override
Stops a recording asyncronously.
bool shouldTrackContentsBeMuted(const Track &t) override
Should return true if this input is currently actively recording into a track and it wants the existi...
bool isRecording() override
Returns true if there are any active recordings for this device.
bool isRecordingActive() const override
Returns true if recording is enabled and the input is connected to any target.
std::shared_ptr< choc::fifo::SingleReaderSingleWriterFIFO< juce::MidiMessage > > getRecordingNotes(EditItemID targetID) const override
Returns a fifo of recorded MIDInotes that can be used for drawing UI components.
juce::Array< Clip * > applyRetrospectiveRecord(bool armedOnly) override
Takes the retrospective buffer and creates clips from it, as if recording had been triggered in the p...
std::vector< tl::expected< std::unique_ptr< RecordingContext >, juce::String > > prepareToRecord(RecordingParameters params) override
Prepares a recording operation.
bool isRecordingQueuedToStop(EditItemID targetID) override
Returns true if the async stopRecording function has been used and this target is waiting to stop.
bool isRecording(EditItemID targetID) override
Returns true if there are any active recordings for this device.
TimePosition getPunchInTime(EditItemID targetID) override
Returns the time that a given target started recording.
tl::expected< Clip::Array, juce::String > stopRecording(StopRecordingParameters params) override
Stops a recording.
bool isRecordingActive(EditItemID targetID) const override
Returns true if recording is enabled and the input is connected the given target.
std::vector< std::unique_ptr< RecordingContext > > startRecording(std::vector< std::unique_ptr< RecordingContext > > newContexts) override
Starts a recording.
Polls a set of targets to see if they should be stopped.
void masterTimeUpdate(double time) override
This is a bit of a hack but allows the time for MIDI devices to be set through the base class interfa...
bool handleIncomingMessage(juce::MidiMessage &)
Updates the timestamp of the message and handles sending it out to listeners.
bool isMPEDevice() const
Returns true if the given device is an MPE device and so should always record incoming MIDI to Note E...
@ none
No automation, add the sequence as plain MIDI with the channel of the clip.
@ expression
Add the automation as EXP assuming the source sequence is MPE MIDI.
Polls a set of targets to see if they should be stopped.
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].
void play(bool justSendMMCIfEnabled)
Starts playback of an Edit.
TimePosition getTimeWhenStarted() const
Returns the time when the transport was started.
bool isPlaying() const
Returns true if the transport is playing.
A basic spin lock that uses an atomic_flag to store the locked state so should never result in a syst...
void unlock() noexcept
Releases the lock, this should only be called after a successful call to try_lock or lock.
bool try_lock() noexcept
Attempts to take the lock once, returning true if successful.
void lock() noexcept
Takes the lock, blocking if necessary.
T empty(T... args)
T end(T... args)
T erase(T... args)
T floor(T... args)
T fmod(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
T lock(T... args)
T lock_shared(T... args)
T max(T... args)
memcpy
T memset(T... args)
Type jlimit(Type lowerLimit, Type upperLimit, Type valueToConstrain) noexcept
juce::String getName(LaunchQType t)
Retuns the name of a LaunchQType for display purposes.
ClipSlot * findClipSlotForID(const Edit &edit, EditItemID id)
Returns the ClipSlot for the given ID.
MidiClip::Ptr insertMIDIClip(ClipOwner &parent, const juce::String &name, TimeRange position)
Inserts a new MidiClip into the ClipOwner's clip list.
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.
juce::Array< AudioTrack * > getTargetTracks(InputDeviceInstance &instance)
Returns the AudioTracks this instance is assigned to.
RangeType< TimePosition > TimeRange
A RangeType based on real time (i.e.
T push_back(T... args)
T remove_if(T... args)
T reserve(T... args)
T reset(T... args)
T resize(T... args)
T size(T... args)
typedef uint8_t
Represents a position in real-life time.
constexpr double inSeconds() const
Returns the TimePosition as a number of seconds.
ID for objects of type EditElement - e.g.
static void reconstructExpression(juce::Array< juce::MidiMessage > &mpeMessagesToAddAtStart, const juce::MidiMessageSequence &data, int trimIndex, int channel)
Reconstruct note expression for a particular channel.
time
#define CRASH_TRACER
This macro adds the current location to a stack which gets logged if a crash happens.
T unlock(T... args)
T unlock_shared(T... args)