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

« « « Anklang Documentation
Loading...
Searching...
No Matches
tracktion_MidiOutputDevice.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
14static bool shouldSendAllControllersOffMessages = true;
15
16//==============================================================================
18{
19public:
21 {
22 update (nullptr);
23 }
24
25 void update (Edit* edit)
26 {
27 framesPerSecond = 24;
28 midiTCType = juce::MidiMessage::fps24;
29 isDropFrame = false;
30 offset = 0;
31 timeBetweenMessages = 0.1;
32
33 if (edit != nullptr)
34 {
35 framesPerSecond = edit->getTimecodeFormat().getFPS();
36 offset = edit->getTimecodeOffset().inSeconds();
37
38 if (framesPerSecond == 25)
39 midiTCType = juce::MidiMessage::fps25;
40 else if (framesPerSecond == 30)
41 midiTCType = juce::MidiMessage::fps30drop;
42
43 timeBetweenMessages = 1.0 / (framesPerSecond * 4);
44 }
45 }
46
47 void addMessages (bool isPlaying, bool isScrubbing,
49 MidiMessageArray& buffer,
50 double blockStart, double blockEnd)
51 {
53 if (isScrubbing || ! isPlaying)
54 {
55 auto len = blockEnd - blockStart;
56
57 blockStart = tc != nullptr ? tc->getPosition().inSeconds() : 0.0;
58 blockEnd = blockStart + len;
59 }
60
61 if (playing != isPlaying
62 || scrubbing != isScrubbing)
63 {
64 playing = isPlaying;
65 scrubbing = isScrubbing;
66
67 updateParts (blockStart);
68 buffer.addMidiMessage (juce::MidiMessage::fullFrame (hours, minutes, seconds, frames, midiTCType),
69 0, MidiMessageArray::notMPE);
70 lastMessageSent = juce::Time::getMillisecondCounter();
71 }
72 else if (lastTime != blockStart)
73 {
74 lastTime = blockStart;
75
76 if (scrubbing)
77 {
79
80 if (now > lastMessageSent + 50)
81 {
82 updateParts (blockStart);
83 buffer.addMidiMessage (juce::MidiMessage::fullFrame (hours, minutes, seconds, frames, midiTCType),
84 0, MidiMessageArray::notMPE);
85 lastMessageSent = now;
86 }
87 }
88 else
89 {
90 auto sequenceNum = (int) std::floor (blockStart / timeBetweenMessages);
91 auto t = sequenceNum * timeBetweenMessages;
92
93 while (t < blockEnd)
94 {
95 if (t >= blockStart)
96 {
97 updateParts (t);
98
99 int value = 0;
100 auto sequenceIndex = (sequenceNum + 65536) & 7;
101
102 switch (sequenceIndex)
103 {
104 case 0: value = frames & 0x0f; break;
105 case 1: value = (frames >> 4); break;
106 case 2: value = seconds & 0x0f; break;
107 case 3: value = (seconds >> 4); break;
108 case 4: value = minutes & 0x0f; break;
109 case 5: value = (minutes >> 4); break;
110 case 6: value = hours & 0x0f; break;
111
112 case 7:
113 value = ((hours >> 4) & 1);
114
115 if (framesPerSecond == 25)
116 {
117 value |= 0x02;
118 }
119 else if (framesPerSecond == 30)
120 {
121 if (isDropFrame)
122 value |= 0x04;
123 else
124 value |= 0x06;
125 }
126
127 break;
128 }
129
130 buffer.addMidiMessage (juce::MidiMessage::quarterFrame (sequenceIndex, value),
131 t - blockStart, MidiMessageArray::notMPE);
132 }
133
134 sequenceNum++;
135 t += timeBetweenMessages;
136 }
137 }
138 }
139 }
140
141private:
142 bool playing = false, scrubbing = false;
143 double lastTime = 0, offset = 0, timeBetweenMessages = 0;
144 uint32_t lastMessageSent = 0;
145 int framesPerSecond = 0;
146 bool isDropFrame = false;
147 juce::MidiMessage::SmpteTimecodeType midiTCType = juce::MidiMessage::fps24;
148 int hours = 0, minutes = 0, seconds = 0, frames = 0;
149
150 void updateParts (double t)
151 {
152 const double nudge = 0.05 / 96000.0;
153
154 t = std::max (0.0, t + offset) + nudge;
155
156 frames = ((int) (t * framesPerSecond)) % framesPerSecond;
157 hours = (int) (t * (1.0 / 3600.0));
158 minutes = (((int) t) / 60) % 60;
159 seconds = (((int) t) % 60);
160 }
161
163};
164
165//==============================================================================
167{
168public:
170 {
171 reset (nullptr);
172 }
173
174 void reset (Edit* edit)
175 {
176 const juce::ScopedLock sl (positionLock);
177 wasPlaying = false;
178 position = nullptr;
179 needsToSendPosition = true;
180 lastBlockStart = -100000.0s;
181 lastBlockEndPPQ = 0;
182
183 if (edit != nullptr)
185 }
186
187 void addMessages (bool playHeadIsPlaying, TransportControl* tc, MidiMessageArray& buffer,
188 TimePosition blockStartTime, TimeDuration blockLength)
189 {
191 const bool isPlaying = playHeadIsPlaying && position != nullptr;
192
193 if (isPlaying != wasPlaying)
194 {
195 wasPlaying = isPlaying;
196 needsToSendPosition = true;
197
198 if (! isPlaying)
199 buffer.addMidiMessage (juce::MidiMessage::midiStop(), 0, MidiMessageArray::notMPE);
200 }
201
202 if (isPlaying)
203 {
204 const juce::ScopedLock sl (positionLock);
205
206 position->set (blockStartTime);
207 auto blockStartPPQ = position->getPPQTime();
208
209 position->set (blockStartTime + blockLength);
210 auto endPPQ = position->getPPQTime();
211
212 const bool jumped = std::abs (lastBlockEndPPQ - endPPQ) > 0.4;
213 lastBlockEndPPQ = endPPQ;
214
215 bool looped = blockStartTime < lastBlockStart;
216 lastBlockStart = blockStartTime;
217
218 needsToSendPosition = needsToSendPosition || jumped || looped;
219
220 const double startNum = blockStartPPQ * 24.0;
221 const double endNum = endPPQ * 24.0;
222 const double timePerNum = blockLength.inSeconds() / (endNum - startNum);
223 auto num = (int) std::floor (startNum + 0.999999);
224
225 while (num < endNum)
226 {
227 if (needsToSendPosition)
228 {
229 if ((num % 24) == 0)
230 {
231 buffer.addMidiMessage (juce::MidiMessage::songPositionPointer (num / 6), 0);
232
233 if (num == 0 || (tc != nullptr && tc->isRecording()))
234 buffer.addMidiMessage (juce::MidiMessage::midiStart(), 0, MidiMessageArray::notMPE);
235 else
236 buffer.addMidiMessage (juce::MidiMessage::midiContinue(), 0, MidiMessageArray::notMPE);
237
238 needsToSendPosition = false;
239 }
240 }
241
242 if (! needsToSendPosition)
243 buffer.addMidiMessage (juce::MidiMessage::midiClock(),
244 (num - startNum) * timePerNum,
245 MidiMessageArray::notMPE);
246
247 ++num;
248 }
249 }
250 }
251
252private:
253 bool wasPlaying = false;
254 bool needsToSendPosition = false;
255 juce::CriticalSection positionLock;
257 TimePosition lastBlockStart;
258 double lastBlockEndPPQ = 0;
259
261};
262
263//==============================================================================
264MidiOutputDevice::MidiOutputDevice (Engine& e, juce::MidiDeviceInfo info)
265 : OutputDevice (e, NEEDS_TRANS("MIDI Output"), info.name, info.identifier),
266 deviceInfo (std::move (info))
267{
268 enabled = true;
269
270 timecodeGenerator = std::make_unique<MidiTimecodeGenerator>();
271 midiClockGenerator = std::make_unique<MidiClockGenerator>();
272 programNameSet = getMidiProgramManager().getDefaultCustomName();
273
274 loadProps();
275 shouldSendAllControllersOffMessages = getControllerOffMessagesSent (engine);
276}
277
278MidiOutputDevice::~MidiOutputDevice()
279{
280 notifyListenersOfDeletion();
281 closeDevice();
282}
283
284void MidiOutputDevice::setEnabled (bool b)
285{
286 if (b != enabled)
287 {
288 enabled = b;
289 saveProps();
290 engine.getDeviceManager().rescanMidiDeviceList();
291 }
292}
293
294juce::String MidiOutputDevice::prepareToPlay (Edit* edit, TimePosition)
295{
296 if (outputDevice == nullptr)
297 return TRANS("Couldn't open the MIDI port");
298
299 stop();
300
301 timecodeGenerator->update (edit);
302 midiClockGenerator->reset (edit);
303 sampleRate = engine.getDeviceManager().getSampleRate();
304
305 return {};
306}
307
308bool MidiOutputDevice::start()
309{
310 if (outputDevice != nullptr)
311 {
312 audioAdjustmentDelay = juce::roundToInt (2.0 * engine.getDeviceManager().getBlockSizeMs());
313 playing = true;
314 return true;
315 }
316
317 return false;
318}
319
320void MidiOutputDevice::stop()
321{
322 if (outputDevice != nullptr)
323 {
324 if (playing)
325 {
326 playing = false;
327 sendNoteOffMessages();
328 }
329 }
330}
331
332void MidiOutputDevice::setControllerOffMessagesSent (Engine& e, bool b)
333{
334 shouldSendAllControllersOffMessages = b;
335 e.getPropertyStorage().setProperty (SettingID::sendControllerOffMessages, b);
336}
337
338bool MidiOutputDevice::getControllerOffMessagesSent (Engine& e)
339{
340 return e.getPropertyStorage().getProperty (SettingID::sendControllerOffMessages, true);
341}
342
343juce::String MidiOutputDevice::getNameForMidiNoteNumber (int note, int midiChannel, bool useSharp) const
344{
345 return midiChannel == 10 ? TRANS(juce::MidiMessage::getRhythmInstrumentName (note))
346 : juce::MidiMessage::getMidiNoteName (note, useSharp, true,
347 engine.getEngineBehaviour().getMiddleCOctave());
348}
349
350void MidiOutputDevice::updateMidiTC (Edit* edit)
351{
352 timecodeGenerator->update (edit);
353}
354
355void MidiOutputDevice::setSendingMMC (bool b)
356{
357 sendingMMC = b;
358}
359
361{
362 TRACKTION_LOG ("MIDI External controller assigned: " + getName());
363 externalController = ec;
364}
365
366void MidiOutputDevice::removeExternalController (ExternalController* ec)
367{
368 if (externalController == ec)
369 externalController = nullptr;
370}
371
372void MidiOutputDevice::loadProps()
373{
374 preDelayMillisecs = 0;
375 sendTimecode = false;
376 sendMidiClock = false;
377 sendControllerMidiClock = false;
378
379 if (auto n = engine.getPropertyStorage().getXmlPropertyItem (SettingID::midiout, getName()))
380 {
381 enabled = n->getBoolAttribute ("enabled", enabled);
382 preDelayMillisecs = n->getIntAttribute ("preDelay", preDelayMillisecs);
383 sendTimecode = n->getBoolAttribute ("sendTimecode", sendTimecode);
384 sendMidiClock = n->getBoolAttribute ("sendMidiClock", sendMidiClock);
385 timecodeGenerator->update (nullptr);
386
387 if (getName() == "Microsoft GS Wavetable SW Synth")
388 programNameSet = n->getStringAttribute ("programNames", TRANS("General MIDI"));
389 else
390 programNameSet = n->getStringAttribute ("programNames", getMidiProgramManager().getDefaultCustomName());
391 }
392}
393
394void MidiOutputDevice::saveProps()
395{
396 juce::XmlElement n ("SETTINGS");
397
398 n.setAttribute ("enabled", enabled);
399 n.setAttribute ("preDelay", preDelayMillisecs);
400 n.setAttribute ("sendTimecode", sendTimecode);
401 n.setAttribute ("sendMidiClock", sendMidiClock);
402 n.setAttribute ("programNames", programNameSet);
403
404 engine.getPropertyStorage().setXmlPropertyItem (SettingID::midiout, getName(), n);
405}
406
407juce::String MidiOutputDevice::openDevice()
408{
409 if (isEnabled())
410 {
411 if (outputDevice == nullptr)
412 {
414 TRACKTION_LOG ("opening MIDI out device: " + getDeviceID() + " (" + getName() + ")");
415
416 if (softDevice)
417 {
418 #if JUCE_MAC
419 outputDevice = juce::MidiOutput::createNewDevice (getName());
420 #else
421 outputDevice.reset();
423 #endif
424 }
425 else if (deviceInfo.identifier.isNotEmpty())
426 {
427 outputDevice = juce::MidiOutput::openDevice (deviceInfo.identifier);
428
429 if (outputDevice == nullptr)
430 {
431 TRACKTION_LOG_ERROR ("Failed to open MIDI output " + getName());
432 return TRANS("Couldn't open device");
433 }
434 }
435 else
436 {
437 outputDevice.reset();
438 }
439 }
440 }
441
442 return {};
443}
444
445void MidiOutputDevice::closeDevice()
446{
447 saveProps();
448
449 if (outputDevice != nullptr)
450 {
451 TRACKTION_LOG ("closing MIDI output: " + getName());
452 outputDevice = nullptr;
453 }
454}
455
456void MidiOutputDevice::sendNoteOffMessages()
457{
458 if (isConnectedToExternalController())
459 return;
460
461 if (outputDevice != nullptr)
462 {
463 const juce::ScopedLock sl (noteOnLock);
464
465 for (int channel = channelsUsed.getHighestBit() + 1; --channel > 0;)
466 {
467 if (channelsUsed [channel])
468 {
469 for (int note = midiNotesOn.getHighestBit() + 1; --note >= 0;)
470 if (midiNotesOn [note])
471 sendMessageNow (juce::MidiMessage::noteOff (channel, note));
472
473 if (sustain > 0)
474 {
475 sendMessageNow (juce::MidiMessage::controllerEvent (channel, 64, 0));
476 sendMessageNow (juce::MidiMessage::controllerEvent (channel, 64, sustain));
477 }
478
479 sendMessageNow (juce::MidiMessage::allNotesOff (channel));
480
481 if (shouldSendAllControllersOffMessages)
482 sendMessageNow (juce::MidiMessage::allControllersOff (channel));
483 }
484 }
485
486 channelsUsed.clear();
487 midiNotesOn.clear();
488 }
489}
490
491void MidiOutputDevice::sendMessageNow (const juce::MidiMessage& message)
492{
493 if (outputDevice != nullptr)
494 outputDevice->sendMessageNow (message);
495}
496
497void MidiOutputDevice::fireMessage (const juce::MidiMessage& message)
498{
499 if (! message.isMetaEvent())
500 {
501 sendMessageNow (message);
502
503 if (message.isNoteOnOrOff())
504 {
505 const juce::ScopedLock sl (noteOnLock);
506
507 if (message.isNoteOn())
508 midiNotesOn.setBit (message.getNoteNumber());
509 else if (message.isNoteOff())
510 midiNotesOn.clearBit (message.getNoteNumber());
511
512 channelsUsed.setBit (message.getChannel());
513 }
514 else if (message.isController() && message.getControllerNumber() == 64)
515 {
516 sustain = message.getControllerValue();
517 }
518 }
519}
520
521TimeDuration MidiOutputDevice::getDeviceDelay() const noexcept
522{
523 return TimeDuration::fromSeconds ((preDelayMillisecs + audioAdjustmentDelay) * 0.001);
524}
525
526void MidiOutputDevice::setPreDelayMs (int ms)
527{
528 if (preDelayMillisecs != ms)
529 {
530 preDelayMillisecs = ms;
532 changed();
533 saveProps();
534 }
535}
536
537void MidiOutputDevice::setSendingClock (bool b)
538{
539 if (sendMidiClock != b)
540 {
541 sendMidiClock = b;
542 changed();
543 saveProps();
544 }
545}
546
547void MidiOutputDevice::flipSendingTimecode()
548{
549 sendTimecode = ! sendTimecode;
550 changed();
551 saveProps();
552}
553
554juce::StringArray MidiOutputDevice::getProgramSets() const
555{
556 return getMidiProgramManager().getMidiProgramSetNames();
557}
558
559int MidiOutputDevice::getCurrentSetIndex() const
560{
561 return getMidiProgramManager().getSetIndex (programNameSet);
562}
563
564void MidiOutputDevice::setCurrentProgramSet (const juce::String& newSet)
565{
566 if (programNameSet != newSet)
567 {
568 programNameSet = newSet;
569 changed();
570 SelectionManager::refreshAllPropertyPanels();
571 }
572}
573
574juce::String MidiOutputDevice::getProgramName (int programNumber, int bank)
575{
576 return getMidiProgramManager().getProgramName (getCurrentSetIndex(), bank, programNumber);
577}
578
579bool MidiOutputDevice::canEditProgramSet (int index) const
580{
581 return getMidiProgramManager().canEditProgramSet (index);
582}
583
584bool MidiOutputDevice::canDeleteProgramSet (int index) const
585{
586 return getMidiProgramManager().canDeleteProgramSet (index);
587}
588
589juce::String MidiOutputDevice::getBankName (int bank)
590{
591 return getMidiProgramManager().getBankName (getCurrentSetIndex(), bank);
592}
593
594int MidiOutputDevice::getBankID (int bank)
595{
596 return getMidiProgramManager().getBankID (getCurrentSetIndex(), bank);
597}
598
599bool MidiOutputDevice::areMidiPatchesZeroBased()
600{
601 return getMidiProgramManager().isZeroBased (getCurrentSetIndex());
602}
603
604MidiOutputDeviceInstance* MidiOutputDevice::createInstance (EditPlaybackContext& c)
605{
606 return new MidiOutputDeviceInstance (*this, c);
607}
608
609//==============================================================================
610MidiOutputDeviceInstance::MidiOutputDeviceInstance (MidiOutputDevice& d, EditPlaybackContext& e)
611 : OutputDeviceInstance (d, e)
612{
613 timecodeGenerator = std::make_unique<MidiTimecodeGenerator>();
614 midiClockGenerator = std::make_unique<MidiClockGenerator>();
615}
616
617MidiOutputDeviceInstance::~MidiOutputDeviceInstance()
618{
619}
620
621juce::String MidiOutputDeviceInstance::prepareToPlay (TimePosition, bool shouldSendMidiTC)
622{
623 if (getMidiOutput().outputDevice == nullptr)
624 return TRANS("Couldn't open the MIDI port");
625
626 stop();
627
628 shouldSendMidiTimecode = shouldSendMidiTC;
629 timecodeGenerator->update (&edit);
630 midiClockGenerator->reset (&edit);
631
632 sampleRate = edit.engine.getDeviceManager().getSampleRate();
633
634 return {};
635}
636
637bool MidiOutputDeviceInstance::start()
638{
639 if (getMidiOutput().outputDevice != nullptr)
640 {
641 audioAdjustmentDelay = juce::roundToInt (2.0 * edit.engine.getDeviceManager().getBlockSizeMs());
642 playing = true;
643 return true;
644 }
645
646 return false;
647}
648
649void MidiOutputDeviceInstance::stop()
650{
651 if (playing)
652 {
653 playing = false;
654 getMidiOutput().sendNoteOffMessages();
655 }
656}
657
658void MidiOutputDeviceInstance::mergeInMidiMessages (const MidiMessageArray& source, TimePosition editTime)
659{
660 midiMessages.mergeFromWithOffset (source, (editTime + getMidiOutput().getDeviceDelay()).inSeconds());
661 midiMessages.sortByTimestamp();
662}
663
664void MidiOutputDeviceInstance::addMidiClockMessagesToCurrentBlock (bool isPlaying, bool isDragging, TimeRange editTimeRange)
665{
666 auto& midiOut = getMidiOutput();
667
668 if (shouldSendMidiTimecode)
669 {
670 if (midiOut.sendTimecode)
671 timecodeGenerator->addMessages (isPlaying, isDragging,
672 &edit.getTransport(), midiMessages,
673 editTimeRange.getStart().inSeconds(),
674 editTimeRange.getEnd().inSeconds());
675
676 if (midiOut.sendMidiClock || midiOut.sendControllerMidiClock)
677 midiClockGenerator->addMessages (isPlaying,
678 &edit.getTransport(), midiMessages,
679 editTimeRange.getStart(),
680 editTimeRange.getLength());
681 }
682}
683
684}} // namespace tracktion { inline namespace engine
BigInteger & clear() noexcept
int getHighestBit() const noexcept
BigInteger & clearBit(int bitNumber) noexcept
BigInteger & setBit(int bitNumber)
static MidiMessage midiStart() noexcept
bool isNoteOn(bool returnTrueForVelocity0=false) const noexcept
int getChannel() const noexcept
bool isController() const noexcept
int getControllerNumber() const noexcept
bool isNoteOff(bool returnTrueForNoteOnVelocity0=true) const noexcept
static MidiMessage quarterFrame(int sequenceNumber, int value) noexcept
static MidiMessage midiStop() noexcept
int getNoteNumber() const noexcept
static MidiMessage allNotesOff(int channel) noexcept
static MidiMessage controllerEvent(int channel, int controllerType, int value) noexcept
static MidiMessage midiClock() noexcept
static MidiMessage fullFrame(int hours, int minutes, int seconds, int frames, SmpteTimecodeType timecodeType)
bool isNoteOnOrOff() const noexcept
static MidiMessage midiContinue() noexcept
int getControllerValue() const noexcept
static MidiMessage noteOff(int channel, int noteNumber, float velocity) noexcept
bool isMetaEvent() const noexcept
static const char * getRhythmInstrumentName(int midiNoteNumber)
static MidiMessage songPositionPointer(int positionInMidiBeats) noexcept
static MidiMessage allControllersOff(int channel) noexcept
static std::unique_ptr< MidiOutput > openDevice(const String &deviceIdentifier)
static std::unique_ptr< MidiOutput > createNewDevice(const String &deviceName)
bool isNotEmpty() const noexcept
static uint32 getMillisecondCounter() noexcept
The Tracktion Edit class!
TimeDuration getTimecodeOffset() const noexcept
Returns the offset to apply to MIDI timecode.
TimecodeDisplayFormat getTimecodeFormat() const
Returns the current TimecodeDisplayFormat.
TempoSequence tempoSequence
The global TempoSequence of this Edit.
The Engine is the central class for all tracktion sessions.
PropertyStorage & getPropertyStorage() const
Returns the PropertyStorage user settings customisable XML file.
DeviceManager & getDeviceManager() const
Returns the DeviceManager instance for handling audio / MIDI devices.
Acts as a holder for a ControlSurface object.
void setExternalController(ExternalController *)
sets the external controller messages are coming from
Base class for audio or midi output devices, to which a track's output can be sent.
virtual void changed()
This should be called to send a change notification to any SelectableListeners that are registered wi...
Controls the transport of an Edit's playback.
TimePosition getPosition() const
Returns the current transport position.
bool isRecording() const
Returns true if recording is in progress.
static std::vector< std::unique_ptr< ScopedContextAllocator > > restartAllTransports(Engine &, bool clearDevices)
Restarts all TransportControl[s] in the Edit.
T floor(T... args)
T is_pointer_v
#define TRANS(stringLiteral)
#define NEEDS_TRANS(stringLiteral)
#define JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(className)
#define jassertfalse
typedef int
T max(T... args)
int roundToInt(const FloatType value) noexcept
tempo::Sequence::Position createPosition(const TempoSequence &s)
Creates a Position to iterate over the given TempoSequence.
T reset(T... args)
typedef uint32_t
Represents a duration in real-life time.
constexpr double inSeconds() const
Returns the TimeDuration as a number of seconds.
Represents a position in real-life time.
constexpr double inSeconds() const
Returns the TimePosition as a number of seconds.
#define CRASH_TRACER
This macro adds the current location to a stack which gets logged if a crash happens.