MayaFlux 0.4.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
CoreMidiBackend.cpp
Go to the documentation of this file.
1#include "CoreMidiBackend.hpp"
2
3#ifdef MAYAFLUX_PLATFORM_MACOS
4
6
7namespace MayaFlux::Core {
8
9namespace {
10 std::string cfstring_to_string(CFStringRef str)
11 {
12 if (!str)
13 return {};
14
15 char buffer[512];
16
17 if (CFStringGetCString(
18 str,
19 buffer,
20 sizeof(buffer),
21 kCFStringEncodingUTF8)) {
22 return buffer;
23 }
24
25 return {};
26 }
27
28 constexpr auto C = Journal::Component::Core;
29 constexpr auto X = Journal::Context::InputBackend;
30}
31
32CoreMidiBackend::CoreMidiBackend()
33 : CoreMidiBackend(Config {})
34{
35}
36
37CoreMidiBackend::CoreMidiBackend(Config config)
38 : m_config(std::move(config))
39{
40}
41
42CoreMidiBackend::~CoreMidiBackend()
43{
44 if (m_initialized.load()) {
45 shutdown();
46 }
47}
48
49bool CoreMidiBackend::initialize()
50{
51 if (m_initialized.load()) {
52 MF_WARN(C, X,
53 "CoreMidiBackend already initialized");
54 return true;
55 }
56
57 OSStatus status = MIDIClientCreate(
58 CFSTR("MayaFlux"),
59 &CoreMidiBackend::midi_notify_callback,
60 this,
61 &m_client);
62
63 if (status != noErr) {
64 MF_ERROR(C, X,
65 "Failed to create CoreMIDI client (status={})",
66 static_cast<int>(status));
67 return false;
68 }
69
70 m_initialized.store(true);
71
72 refresh_devices();
73
74 create_virtual_port_if_enabled();
75
76 MF_INFO(C, X,
77 "CoreMidiBackend initialized with {} port(s)",
78 m_enumerated_devices.size());
79
80 return true;
81}
82
83void CoreMidiBackend::start()
84{
85 if (!m_initialized.load()) {
86 MF_ERROR(C, X,
87 "Cannot start CoreMidiBackend: not initialized");
88 return;
89 }
90
91 if (m_running.load()) {
92 MF_WARN(C, X,
93 "CoreMidiBackend already running");
94 return;
95 }
96
97 if (m_config.auto_open_inputs) {
98 std::vector<uint32_t> to_open;
99
100 {
101 std::lock_guard lock(m_devices_mutex);
102
103 for (const auto& [id, info] : m_enumerated_devices) {
104 to_open.push_back(id);
105 }
106 }
107
108 for (uint32_t id : to_open) {
109 open_device(id);
110 }
111 }
112
113 m_running.store(true);
114
115 MF_INFO(C, X,
116 "CoreMidiBackend started with {} open port(s)",
117 get_open_devices().size());
118}
119
120void CoreMidiBackend::stop()
121{
122 if (!m_running.load())
123 return;
124
125 std::vector<uint32_t> to_close;
126
127 {
128 std::lock_guard lock(m_devices_mutex);
129
130 for (const auto& [id, state] : m_open_devices) {
131 to_close.push_back(id);
132 }
133 }
134
135 for (uint32_t id : to_close) {
136 close_device(id);
137 }
138
139 m_running.store(false);
140
141 MF_INFO(C, X, "CoreMidiBackend stopped");
142}
143
144void CoreMidiBackend::shutdown()
145{
146 if (!m_initialized.load())
147 return;
148
149 stop();
150
151 if (m_virtual_destination) {
152 MIDIEndpointDispose(m_virtual_destination);
153 m_virtual_destination = 0;
154 }
155
156 {
157 std::lock_guard lock(m_devices_mutex);
158 m_open_devices.clear();
159 m_enumerated_devices.clear();
160 }
161
162 if (m_client) {
163 MIDIClientDispose(m_client);
164 m_client = 0;
165 }
166
167 m_initialized.store(false);
168
169 MF_INFO(C, X, "CoreMidiBackend shutdown complete");
170}
171
172std::vector<InputDeviceInfo> CoreMidiBackend::get_devices() const
173{
174 std::lock_guard lock(m_devices_mutex);
175
176 std::vector<InputDeviceInfo> result;
177 result.reserve(m_enumerated_devices.size());
178
179 for (const auto& [id, info] : m_enumerated_devices) {
180 result.push_back(info);
181 }
182
183 return result;
184}
185
186size_t CoreMidiBackend::refresh_devices()
187{
188 std::vector<InputDeviceInfo> newly_added;
189 std::vector<InputDeviceInfo> removed_devices;
190
191 const ItemCount count = MIDIGetNumberOfSources();
192
193 std::unordered_set<MIDIUniqueID> seen;
194
195 {
196 std::lock_guard lock(m_devices_mutex);
197
198 for (ItemCount i = 0; i < count; ++i) {
199 MIDIEndpointRef endpoint = MIDIGetSource(i);
200 if (!endpoint)
201 continue;
202
203 MIDIUniqueID unique_id = 0;
204 if (MIDIObjectGetIntegerProperty(endpoint, kMIDIPropertyUniqueID, &unique_id) != noErr)
205 continue;
206
207 seen.insert(unique_id);
208
209 CFStringRef name_ref = nullptr;
210 MIDIObjectGetStringProperty(endpoint, kMIDIPropertyName, &name_ref);
211 std::string cname = cfstring_to_string(name_ref);
212
213 if (name_ref)
214 CFRelease(name_ref);
215
216 if (!port_matches_filter(cname))
217 continue;
218
219 uint32_t dev_id = find_or_assign_device_id(unique_id);
220 bool is_new = m_enumerated_devices.find(dev_id) == m_enumerated_devices.end();
221
222 MIDIPortInfo info {};
223 info.id = dev_id;
224 info.name = cname;
225 info.unique_id = unique_id;
226 info.endpoint = endpoint;
227 info.backend_type = InputType::MIDI;
228 info.is_connected = true;
229 info.is_input = true;
230 info.is_output = false;
231 info.port_number = static_cast<uint8_t>(i);
232
233 m_enumerated_devices[dev_id] = info;
234 if (is_new)
235 newly_added.push_back(info);
236 }
237
238 for (auto it = m_enumerated_devices.begin();
239 it != m_enumerated_devices.end();) {
240
241 if (!seen.contains(it->second.unique_id)) {
242
243 removed_devices.push_back(it->second);
244
245 auto open_it = m_open_devices.find(it->first);
246
247 if (open_it != m_open_devices.end()) {
248 auto state = std::move(open_it->second);
249
250 destroy_open_port(*state);
251
252 m_open_devices.erase(open_it);
253 }
254
255 it = m_enumerated_devices.erase(it);
256 } else {
257 ++it;
258 }
259 }
260 }
261
262 for (const auto& info : newly_added) {
263 MF_INFO(C, X, "MIDI port found: '{}'", info.name);
264 notify_device_change(info, true);
265 }
266
267 for (const auto& info : removed_devices) {
268 MF_INFO(C, X, "MIDI port removed: '{}'", info.name);
269 notify_device_change(info, false);
270 }
271
272 std::lock_guard lock(m_devices_mutex);
273 return m_enumerated_devices.size();
274}
275
276bool CoreMidiBackend::open_device(uint32_t device_id)
277{
278 std::lock_guard lock(m_devices_mutex);
279
280 if (m_open_devices.find(device_id) != m_open_devices.end()) {
281 return true;
282 }
283
284 auto it = m_enumerated_devices.find(device_id);
285
286 if (it == m_enumerated_devices.end()) {
287 return false;
288 }
289
290 auto state = std::make_shared<MIDIPortState>();
291
292 state->info = it->second;
293 state->device_id = device_id;
294
295 {
296 std::lock_guard cb_lock(m_callback_mutex);
297 state->input_callback = m_input_callback;
298 }
299
300 OSStatus status = MIDIInputPortCreate(
301 m_client,
302 CFSTR("MayaFlux Input"),
303 &CoreMidiBackend::midi_read_callback,
304 state.get(),
305 &state->input_port);
306
307 if (status != noErr) {
308 return false;
309 }
310
311 status = MIDIPortConnectSource(
312 state->input_port,
313 state->info.endpoint,
314 state.get());
315
316 if (status != noErr) {
317 MIDIPortDispose(state->input_port);
318 state->input_port = 0;
319 return false;
320 }
321
322 state->active.store(true);
323
324 m_open_devices.insert_or_assign(device_id, state);
325
326 MF_INFO(C, X,
327 "Opened MIDI port {}: '{}'",
328 device_id,
329 state->info.name);
330
331 return true;
332}
333
334void CoreMidiBackend::close_device(uint32_t device_id)
335{
336 std::shared_ptr<MIDIPortState> state;
337
338 {
339 std::lock_guard lock(m_devices_mutex);
340
341 auto it = m_open_devices.find(device_id);
342
343 if (it == m_open_devices.end()) {
344 return;
345 }
346
347 state = std::move(it->second);
348
349 m_open_devices.erase(it);
350 }
351
352 destroy_open_port(*state);
353
354 MF_INFO(C, X,
355 "Closed MIDI port {}: '{}'",
356 device_id,
357 state->info.name);
358}
359
360void CoreMidiBackend::destroy_open_port(MIDIPortState& state)
361{
362 state.active.store(false);
363
364 if (state.input_port) {
365 MIDIPortDisconnectSource(
366 state.input_port,
367 state.info.endpoint);
368
369 MIDIPortDispose(state.input_port);
370
371 state.input_port = 0;
372 }
373}
374
375bool CoreMidiBackend::is_device_open(uint32_t device_id) const
376{
377 std::lock_guard lock(m_devices_mutex);
378
379 return m_open_devices.find(device_id)
380 != m_open_devices.end();
381}
382
383std::vector<uint32_t> CoreMidiBackend::get_open_devices() const
384{
385 std::lock_guard lock(m_devices_mutex);
386
387 std::vector<uint32_t> result;
388 result.reserve(m_open_devices.size());
389
390 for (const auto& [id, state] : m_open_devices) {
391 result.push_back(id);
392 }
393
394 return result;
395}
396
397void CoreMidiBackend::set_input_callback(InputCallback callback)
398{
399 std::lock_guard lock(m_callback_mutex);
400
401 m_input_callback = std::move(callback);
402}
403
404void CoreMidiBackend::set_device_callback(DeviceCallback callback)
405{
406 std::lock_guard lock(m_callback_mutex);
407 m_device_callback = std::move(callback);
408}
409
410bool CoreMidiBackend::port_matches_filter(const std::string& port_name) const
411{
412 if (m_config.input_port_filters.empty())
413 return true;
414
415 return std::ranges::any_of(
416 m_config.input_port_filters,
417 [&port_name](const std::string& filter) {
418 return port_name.find(filter) != std::string::npos;
419 });
420}
421
422uint32_t CoreMidiBackend::find_or_assign_device_id(
423 MIDIUniqueID unique_id)
424{
425 for (const auto& [id, info] : m_enumerated_devices) {
426 if (info.unique_id == unique_id)
427 return id;
428 }
429
430 return m_next_device_id++;
431}
432
433void CoreMidiBackend::create_virtual_port_if_enabled()
434{
435 if (!m_config.enable_virtual_port)
436 return;
437
438 if (m_virtual_destination)
439 return;
440
441 CFStringRef name = CFStringCreateWithCString(
442 nullptr,
443 m_config.virtual_port_name.c_str(),
444 kCFStringEncodingUTF8);
445
446 if (!name) {
447 MF_ERROR(C, X,
448 "Failed to create CoreFoundation string for virtual MIDI destination");
449 return;
450 }
451
452 OSStatus status = MIDIDestinationCreate(
453 m_client,
454 name,
455 &CoreMidiBackend::virtual_destination_callback,
456 this,
457 &m_virtual_destination);
458
459 CFRelease(name);
460
461 if (status != noErr) {
462 MF_ERROR(C, X,
463 "Failed to create virtual MIDI destination '{}' (status={})",
464 m_config.virtual_port_name,
465 static_cast<int>(status));
466 return;
467 }
468
469 MF_INFO(C, X,
470 "Created virtual MIDI destination '{}'",
471 m_config.virtual_port_name);
472}
473
474void CoreMidiBackend::midi_read_callback(
475 const MIDIPacketList* packet_list,
476 void* read_proc_ref_con,
477 void* /*src_conn_ref_con*/)
478{
479 auto* state = static_cast<MIDIPortState*>(read_proc_ref_con);
480
481 if (!state)
482 return;
483
484 if (!state->active.load())
485 return;
486
487 if (!state->input_callback)
488 return;
489
490 const MIDIPacket* packet = &packet_list->packet[0];
491
492 for (UInt32 i = 0; i < packet_list->numPackets; ++i) {
493 if (packet->length == 0) {
494 packet = MIDIPacketNext(packet);
495 continue;
496 }
497
498 if (packet->length > 3) {
499
500 std::vector<uint8_t> bytes(
501 packet->data,
502 packet->data + packet->length);
503
504 state->input_callback(
505 InputValue::make_bytes(
506 std::move(bytes),
507 state->device_id,
508 InputType::MIDI));
509 } else {
510
511 uint8_t status = packet->data[0];
512 uint8_t d1 = packet->length > 1 ? packet->data[1] : 0;
513 uint8_t d2 = packet->length > 2 ? packet->data[2] : 0;
514
515 state->input_callback(
516 InputValue::make_midi(
517 status,
518 d1,
519 d2,
520 state->device_id));
521 }
522
523 packet = MIDIPacketNext(packet);
524 }
525}
526
527void CoreMidiBackend::notify_device_change(
528 const InputDeviceInfo& info,
529 bool connected)
530{
531 std::lock_guard lock(m_callback_mutex);
532
533 if (m_device_callback) {
534 m_device_callback(info, connected);
535 }
536}
537
538void CoreMidiBackend::midi_notify_callback(
539 const MIDINotification* notification,
540 void* ref_con)
541{
542 auto* self = static_cast<CoreMidiBackend*>(ref_con);
543
544 if (!self || !notification) {
545 return;
546 }
547
548 switch (notification->messageID) {
549
550 case kMIDIMsgObjectAdded: {
551 const auto* msg = reinterpret_cast<const MIDIObjectAddRemoveNotification*>(notification);
552
553 MF_INFO(
554 Journal::Component::Core,
555 Journal::Context::InputBackend,
556 "CoreMIDI object added (type={})",
557 static_cast<int>(msg->childType));
558
559 self->refresh_devices();
560 break;
561 }
562
563 case kMIDIMsgObjectRemoved: {
564 const auto* msg = reinterpret_cast<const MIDIObjectAddRemoveNotification*>(notification);
565
566 MF_INFO(
567 Journal::Component::Core,
568 Journal::Context::InputBackend,
569 "CoreMIDI object removed (type={})",
570 static_cast<int>(msg->childType));
571
572 self->refresh_devices();
573 break;
574 }
575
576 case kMIDIMsgPropertyChanged: {
577 const auto* msg = reinterpret_cast<const MIDIObjectPropertyChangeNotification*>(notification);
578
579 std::string property_name;
580
581 if (msg->propertyName) {
582 property_name = cfstring_to_string(msg->propertyName);
583 }
584
585 MF_DEBUG(
586 Journal::Component::Core,
587 Journal::Context::InputBackend,
588 "CoreMIDI property changed: {}",
589 property_name.empty() ? "<unknown>" : property_name);
590
591 self->refresh_devices();
592 break;
593 }
594
595 case kMIDIMsgSetupChanged:
596 MF_INFO(
597 Journal::Component::Core,
598 Journal::Context::InputBackend,
599 "CoreMIDI setup changed");
600
601 self->refresh_devices();
602 break;
603
604 case kMIDIMsgIOError: {
605 const auto* msg = reinterpret_cast<const MIDIIOErrorNotification*>(notification);
606
607 MF_WARN(
608 Journal::Component::Core,
609 Journal::Context::InputBackend,
610 "CoreMIDI I/O error on device {} (error={})",
611 msg->driverDevice,
612 msg->errorCode);
613
614 break;
615 }
616
617 default:
618 MF_DEBUG(
619 Journal::Component::Core,
620 Journal::Context::InputBackend,
621 "Unhandled CoreMIDI notification {}",
622 static_cast<int>(notification->messageID));
623 break;
624 }
625}
626
627void CoreMidiBackend::virtual_destination_callback(
628 const MIDIPacketList* packet_list,
629 void* read_proc_ref_con,
630 void* /*src_conn_ref_con*/)
631{
632 auto* self = static_cast<CoreMidiBackend*>(read_proc_ref_con);
633
634 if (!self)
635 return;
636
637 InputCallback callback;
638
639 {
640 std::lock_guard lock(self->m_callback_mutex);
641 callback = self->m_input_callback;
642 }
643
644 if (!callback)
645 return;
646
647 const MIDIPacket* packet = &packet_list->packet[0];
648
649 for (UInt32 i = 0; i < packet_list->numPackets; ++i) {
650
651 if (packet->length == 0) {
652 packet = MIDIPacketNext(packet);
653 continue;
654 }
655
656 if (packet->length > 3 && self->m_config.enable_sysex) {
657
658 std::vector<uint8_t> bytes(
659 packet->data,
660 packet->data + packet->length);
661
662 callback(
663 InputValue::make_bytes(
664 std::move(bytes),
665 0,
666 InputType::MIDI));
667 } else {
668
669 uint8_t status = packet->data[0];
670 uint8_t d1 = packet->length > 1 ? packet->data[1] : 0;
671 uint8_t d2 = packet->length > 2 ? packet->data[2] : 0;
672
673 callback(
674 InputValue::make_midi(
675 status,
676 d1,
677 d2,
678 0));
679 }
680
681 packet = MIDIPacketNext(packet);
682 }
683}
684
685std::string CoreMidiBackend::get_version() const
686{
687 return "CoreMIDI";
688}
689
690} // namespace MayaFlux::Core
691
692#endif // MAYAFLUX_PLATFORM_MACOS
#define MF_INFO(comp, ctx,...)
#define MF_ERROR(comp, ctx,...)
#define MF_WARN(comp, ctx,...)
#define MF_DEBUG(comp, ctx,...)
size_t count
std::function< void(const InputValue &)> InputCallback
Callback signature for input events.
@ InputBackend
Input device backend (HID, MIDI, OSC)
@ Core
Core engine, backend, subsystems.
void shutdown()
Release stored references.
Definition Forma.cpp:168
void stop()
Stop all Portal::Graphics operations.
Definition Graphics.cpp:69