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

« « « Anklang Documentation
Loading...
Searching...
No Matches
tracktion_PhysicalMidiInputDevice.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
15 private juce::Timer
16{
18 : owner (m), transport (m.edit.getTransport())
19 {
20 }
21
22 bool processMessage (const juce::MidiMessage& m)
23 {
24 if (m.isFullFrame())
25 {
26 m.getFullFrameParameters (hours, minutes, seconds, frames, midiTCType);
27
28 lastTime = getTime();
29 correctedTime = lastTime - owner.edit.getTimecodeOffset().inSeconds();
30
31 postMessage (new TCMessage (1)); // stop
32 jumpPending = true;
33 postMessage (new TCMessage (3)); // go to lastTime
34
35 return true;
36 }
37
38 if (m.isQuarterFrame())
39 {
40 const int value = m.getQuarterFrameValue();
41 int speedComp = 0;
42
43 startTimer (100);
44
45 if (! transport.isPlaying())
46 postMessage (new TCMessage (2)); // play
47
48 switch (m.getQuarterFrameSequenceNumber())
49 {
50 case 0: frames = (frames & 0xf0) | value; break;
51 case 1: frames = (frames & 0x0f) | (value << 4); break;
52 case 2: seconds = (seconds & 0xf0) | value; break;
53 case 3: seconds = (seconds & 0x0f) | (value << 4); break;
54 case 4: minutes = (minutes & 0xf0) | value; break;
55 case 5: minutes = (minutes & 0x0f) | (value << 4); break;
56 case 6: hours = (hours & 0xf0) | value; break;
57
58 case 7:
59 {
60 hours = (hours & 0x0f) | ((value << 4) & 0x10);
61 midiTCType = (juce::MidiMessage::SmpteTimecodeType) (value >> 1);
62
63 lastTime = getTime() + 2.0 / getFPS();
64
65 correctedTime = lastTime - owner.edit.getTimecodeOffset().inSeconds();
66
67 const double drift = correctedTime - owner.context.getPosition().inSeconds();
68
69 averageDrift = averageDrift * 0.9 + drift * 0.1;
70 ++averageDriftNumSamples;
71
72 if (! jumpPending)
73 {
74 if (std::abs (drift) > 2.0)
75 {
76 owner.context.postPosition (TimePosition::fromSeconds (correctedTime));
77 averageDrift = 0.0;
78 averageDriftNumSamples = 0;
79 }
80 else if (std::abs (averageDrift) > 0.05
81 && averageDriftNumSamples > 50)
82 {
83 speedComp = (averageDrift > 0.0) ? 1 : -1;
84 averageDrift = 0.0;
85 averageDriftNumSamples = 0;
86 }
87 }
88
89 if (auto epc = transport.getCurrentPlaybackContext())
90 epc->setSpeedCompensation (speedComp);
91
92 break;
93 }
94
95 default:
96 break;
97 }
98
99 return true;
100 }
101
102 return false;
103 }
104
105 void handleMMCMessage (juce::MidiMessage::MidiMachineControlCommand type)
106 {
107 auto m = new TCMessage (10);
108 m->command = type;
109 postMessage (m);
110 }
111
112 void handleMMCGotoMessage (int h, int m, int s, int f)
113 {
114 auto mess = new TCMessage (11);
115 mess->data[0] = h;
116 mess->data[1] = m;
117 mess->data[2] = s;
118 mess->data[3] = f;
119 postMessage (mess);
120 }
121
122private:
124 TransportControl& transport;
125
126 int hours = 0, minutes = 0, seconds = 0, frames = 0;
128 double lastTime = 0, correctedTime = 0, averageDrift = 0;
129 int averageDriftNumSamples = 0;
130 bool jumpPending = false;
131
132 void timerCallback() override
133 {
134 stopTimer();
135
136 if (transport.isPlaying())
137 {
138 transport.stop (false, false, false);
139 transport.setPosition (TimePosition::fromSeconds (correctedTime));
140 averageDrift = 0.0;
141 averageDriftNumSamples = 0;
142 }
143 }
144
145 struct TCMessage : public juce::Message
146 {
147 TCMessage (int tp) : type (tp) {}
148
149 int type = 0;
151 int data[4];
152 };
153
154 void handleMessage (const juce::Message& message) override
155 {
156 if (auto m = dynamic_cast<const TCMessage*> (&message))
157 {
158 if (m->type == 1) // stop
159 {
160 stopTimer();
161
162 if (transport.isPlaying())
163 {
164 transport.stop (false, false, false);
165 transport.setPosition (TimePosition::fromSeconds (correctedTime));
166 averageDrift = 0.0;
167 averageDriftNumSamples = 0;
168 }
169 }
170 else if (m->type == 2) // play
171 {
172 if (! transport.isPlaying())
173 {
174 transport.play (false);
175 startTimer (200);
176 averageDrift = 0.0;
177 averageDriftNumSamples = 0;
178 }
179 }
180 else if (m->type == 3) // goto last time
181 {
182 transport.setPosition (TimePosition::fromSeconds (correctedTime));
183
184 averageDrift = 0.0;
185 averageDriftNumSamples = 0;
186 jumpPending = false;
187 }
188 else if (m->type == 10) // mmc message
189 {
190 handleMMC (m->command);
191 }
192 else if (m->type == 11) // mmc goto
193 {
194 handleMMCGoto (m->data[0], m->data[1], m->data[2], m->data[3]);
195 }
196 }
197 }
198
200 {
201 switch (type)
202 {
203 case juce::MidiMessage::mmc_stop: transport.stop (false, false, false); break;
204 case juce::MidiMessage::mmc_play: transport.play (false); break;
205 case juce::MidiMessage::mmc_deferredplay: transport.play (false); break;
206 case juce::MidiMessage::mmc_fastforward: transport.nudgeRight(); break;
207 case juce::MidiMessage::mmc_rewind: transport.nudgeLeft(); break;
208 case juce::MidiMessage::mmc_recordStart: transport.record (false); break;
209 case juce::MidiMessage::mmc_recordStop: transport.stop (false, false, false); break;
210
211 case juce::MidiMessage::mmc_pause:
212 if (transport.isPlaying())
213 transport.stop (false, false, false);
214 else
215 transport.play (false);
216
217 break;
218 }
219 }
220
221 void handleMMCGoto (int hours_, int minutes_, int seconds_, int frames_)
222 {
223 const int fps = owner.edit.getTimecodeFormat().getFPS();
224 transport.setPosition (TimePosition::fromSeconds (hours_ * 3600 + minutes_ * 60 + seconds_ + (1.0 / double (fps) * frames_)));
225 }
226
227 double getFPS() const noexcept
228 {
229 if (midiTCType == juce::MidiMessage::fps25) return 25.0;
230 if (midiTCType == juce::MidiMessage::fps24) return 24.0;
231 return 30.0;
232 }
233
234 double getTime() const noexcept
235 {
236 double timeWithoutHours = minutes * 60.0
237 + seconds
238 + frames / getFPS();
239
240 if (auto pmi = dynamic_cast<PhysicalMidiInputDevice*> (&owner.getMidiInput()))
241 if (pmi->isIgnoringHours())
242 return timeWithoutHours;
243
244 return hours * 3600.0 + timeWithoutHours;
245 }
246
248};
249
250//==============================================================================
252{
255 {
256 timecodeReader = std::make_unique<MidiTimecodeReader> (*this);
257 }
258
259 void handleMMCMessage (const juce::MidiMessage& message) override
260 {
261 int hours, minutes, seconds, frames;
262
263 if (message.isMidiMachineControlGoto (hours, minutes, seconds, frames))
264 timecodeReader->handleMMCGotoMessage (hours, minutes, seconds, frames);
265 else
266 timecodeReader->handleMMCMessage (message.getMidiMachineControlCommand());
267 }
268
269 bool handleTimecodeMessage (const juce::MidiMessage& message) override
270 {
271 return timecodeReader->processMessage (message);
272 }
273
275 {
276 if (getPhysicalMidiInput().inputDevice != nullptr)
278
280 res.emplace_back (tl::unexpected (TRANS("Couldn't open the MIDI port")));
281 return res;
282 }
283
284 PhysicalMidiInputDevice& getPhysicalMidiInput() const { return static_cast<PhysicalMidiInputDevice&> (owner); }
285
287
288private:
290};
291
292//==============================================================================
293PhysicalMidiInputDevice::PhysicalMidiInputDevice (Engine& e, juce::MidiDeviceInfo info)
294 : MidiInputDevice (e, TRANS("MIDI Input"), info.name, "midiin_" + juce::String::toHexString (info.identifier.hashCode())),
295 deviceInfo (std::move (info))
296{
297 enabled = true;
298
299 controllerParser = std::make_unique<MidiControllerParser> (e);
300 loadProps();
301}
302
303PhysicalMidiInputDevice::~PhysicalMidiInputDevice()
304{
305 closeDevice();
306}
307
309{
310 if (! isTrackDevice() && retrospectiveBuffer == nullptr)
311 retrospectiveBuffer = std::make_unique<RetrospectiveMidiBuffer> (c.edit.engine);
312
313 return new PhysicalMidiInputDeviceInstance (*this, c);
314}
315
316juce::String PhysicalMidiInputDevice::openDevice()
317{
318 std::memset (keysDown, 0, sizeof (keysDown));
319 std::memset (keyDownVelocities, 0, sizeof (keyDownVelocities));
320
321 if (inputDevice == nullptr)
322 {
324 inputDevice = juce::MidiInput::openDevice (deviceInfo.identifier, this);
325
326 if (inputDevice != nullptr)
327 {
328 TRACKTION_LOG ("opening MIDI in device: " + getDeviceID() + " (" + getName() + ")");
329 inputDevice->start();
330 }
331 }
332
333 if (inputDevice != nullptr)
334 return {};
335
336 return TRANS("Couldn't open the MIDI port");
337}
338
339void PhysicalMidiInputDevice::closeDevice()
340{
341 jassert (instances.isEmpty());
342 instances.clear();
343
344 if (inputDevice != nullptr)
345 {
347 TRACKTION_LOG ("Closing MIDI in device: " + getName());
348 inputDevice = nullptr;
349 }
350
351 saveProps();
352}
353
355{
356 TRACKTION_LOG ("MIDI External controller assigned: " + getName());
357 externalController = ec;
358}
359
360void PhysicalMidiInputDevice::removeExternalController (ExternalController* ec)
361{
362 if (externalController == ec)
363 externalController = nullptr;
364}
365
366bool PhysicalMidiInputDevice::isAvailableToEdit() const
367{
368 return isEnabled() && (externalController == nullptr
369 || ! externalController->eatsAllMessages());
370}
371
372bool PhysicalMidiInputDevice::tryToSendTimecode (const juce::MidiMessage& message)
373{
374 if (isAcceptingMMC && message.isMidiMachineControlMessage())
375 {
376 const juce::ScopedLock sl (instanceLock);
377
378 for (auto p : instances)
379 p->handleMMCMessage (message);
380
381 return true;
382 }
383
384 if (isReadingMidiTimecode)
385 {
386 const juce::ScopedLock sl (instanceLock);
387
388 for (auto p : instances)
389 if (p->handleTimecodeMessage (message))
390 return true;
391 }
392
393 return false;
394}
395
396void PhysicalMidiInputDevice::handleIncomingMidiMessage (const juce::MidiMessage& m)
397{
398 if (minimumLengthMs > 0)
399 {
400 if (m.isNoteOn())
401 {
402 auto idx = (m.getChannel() - 1) + m.getNoteNumber();
403 lastNoteOns[size_t (idx)] = juce::Time::getMillisecondCounterHiRes();
404
405 if (noteDispatcher)
406 noteDispatcher->clear (m.getChannel(), m.getNoteNumber());
407 }
408 else if (m.isNoteOff())
409 {
410 auto idx = (m.getChannel() - 1) + m.getNoteNumber();
412
413 if (now - lastNoteOns[size_t (idx)] < minimumLengthMs)
414 {
415 auto delta = minimumLengthMs - (now - lastNoteOns[size_t (idx)]);
416
417 if (noteDispatcher)
418 noteDispatcher->enqueue (now + delta, m);
419
420 return;
421 }
422 }
423 }
424
425 if (m.isNoteOn())
426 {
427 if (activeNotes.isNoteActive (m.getChannel(), m.getNoteNumber()))
428 {
429 // If the note is on and we get another on, send the off before the on
430 handleIncomingMidiMessageInt (juce::MidiMessage::noteOff (m.getChannel(), m.getNoteNumber(), 0.0f));
431 handleIncomingMidiMessageInt (m);
432 }
433 else
434 {
435 // otherwise send the on and track its on
436 activeNotes.startNote (m.getChannel(), m.getNoteNumber());
437 handleIncomingMidiMessageInt (m);
438 }
439 }
440 else if (m.isNoteOff())
441 {
442 // Ignore all note offs unless the note was already on
443 if (activeNotes.isNoteActive (m.getChannel(), m.getNoteNumber()))
444 {
445 activeNotes.clearNote (m.getChannel(), m.getNoteNumber());
446 handleIncomingMidiMessageInt (m);
447 }
448 }
449 else
450 {
451 handleIncomingMidiMessageInt (m);
452 }
453}
454
455void PhysicalMidiInputDevice::handleIncomingMidiMessageInt (const juce::MidiMessage& m)
456{
457 {
458 const std::scoped_lock sl (listenerLock);
459 listeners.call ([m] (Listener& l) { l.handleIncomingMidiMessage (m); });
460 }
461
462 if (externalController != nullptr && externalController->wantsMessage (*this, m))
463 {
464 externalController->acceptMidiMessage (*this, m);
465 }
466 else
467 {
468 auto message = m;
469
470 if (handleIncomingMessage (message))
471 {
472 if (! tryToSendTimecode (message))
473 {
474 if (isTakingControllerMessages)
475 controllerParser->processMessage (message);
476
477 sendMessageToInstances (message);
478 }
479 }
480
481 engine.getDeviceManager().broadcastMessageToAllVirtualDevices (*this, m);
482 }
483}
484
485void PhysicalMidiInputDevice::loadProps()
486{
487 isTakingControllerMessages = true;
488
489 auto n = engine.getPropertyStorage().getXmlPropertyItem (SettingID::midiin, getName());
490
491 if (n != nullptr)
492 isTakingControllerMessages = n->getBoolAttribute ("controllerMessages", isTakingControllerMessages);
493
494 MidiInputDevice::loadMidiProps (n.get());
495}
496
497void PhysicalMidiInputDevice::saveProps()
498{
499 if (isTrackDevice())
500 return;
501
502 juce::XmlElement n ("SETTINGS");
503 n.setAttribute ("controllerMessages", isTakingControllerMessages);
504
505 MidiInputDevice::saveMidiProps (n);
506
507 engine.getPropertyStorage().setXmlPropertyItem (SettingID::midiin, getName(), n);
508}
509
510void PhysicalMidiInputDevice::setReadingMidiTimecode (bool b)
511{
512 isReadingMidiTimecode = b;
513}
514
515void PhysicalMidiInputDevice::setIgnoresHours (bool b)
516{
517 ignoreHours = b;
518}
519
520void PhysicalMidiInputDevice::setAcceptingMMC (bool b)
521{
522 isAcceptingMMC = b;
523}
524
525void PhysicalMidiInputDevice::setReadingControllerMessages (bool b)
526{
527 isTakingControllerMessages = b;
528}
529
530}} // namespace tracktion { inline namespace engine
void postMessage(Message *message) const
static std::unique_ptr< MidiInput > openDevice(const String &deviceIdentifier, MidiInputCallback *callback)
bool isMidiMachineControlMessage() const noexcept
static MidiMessage noteOff(int channel, int noteNumber, float velocity) noexcept
bool isMidiMachineControlGoto(int &hours, int &minutes, int &seconds, int &frames) const noexcept
MidiMachineControlCommand getMidiMachineControlCommand() const noexcept
static double getMillisecondCounterHiRes() noexcept
void stopTimer() noexcept
void startTimer(int intervalInMilliseconds) noexcept
void postPosition(TimePosition positionToJumpTo, std::optional< TimePosition > whenToJump={})
Posts a transport position change.
TimeDuration getTimecodeOffset() const noexcept
Returns the offset to apply to MIDI timecode.
TimecodeDisplayFormat getTimecodeFormat() const
Returns the current TimecodeDisplayFormat.
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.
An instance of an InputDevice that's available to an Edit.
Edit & edit
The Edit this instance belongs to.
EditPlaybackContext & context
The EditPlaybackContext this instance belongs to.
InputDevice & owner
The state of this instance.
std::vector< tl::expected< std::unique_ptr< RecordingContext >, juce::String > > prepareToRecord(RecordingParameters params) override
Prepares a recording operation.
bool handleIncomingMessage(juce::MidiMessage &)
Updates the timestamp of the message and handles sending it out to listeners.
void setExternalController(ExternalController *)
sets the external controller to send the messages to.
InputDeviceInstance * createInstance(EditPlaybackContext &) override
Creates an instance to use for a given playback context.
Controls the transport of an Edit's playback.
EditPlaybackContext * getCurrentPlaybackContext() const
Returns the active EditPlaybackContext if this Edit is attached to the DeviceManager for playback.
bool isPlaying() const
Returns true if the transport is playing.
T emplace_back(T... args)
T is_pointer_v
#define TRANS(stringLiteral)
#define jassert(expression)
#define JUCE_DECLARE_NON_COPYABLE(className)
#define JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(className)
T memset(T... args)
constexpr double inSeconds() const
Returns the TimeDuration as a number of seconds.
constexpr double inSeconds() const
Returns the TimePosition as a number of seconds.
std::vector< tl::expected< std::unique_ptr< RecordingContext >, juce::String > > prepareToRecord(RecordingParameters params) override
Prepares a recording operation.
typedef size_t
#define CRASH_TRACER
This macro adds the current location to a stack which gets logged if a crash happens.