3#ifdef MAYAFLUX_PLATFORM_MACOS
10 std::string cfstring_to_string(CFStringRef str)
17 if (CFStringGetCString(
21 kCFStringEncodingUTF8)) {
32CoreMidiBackend::CoreMidiBackend()
33 : CoreMidiBackend(Config {})
37CoreMidiBackend::CoreMidiBackend(Config config)
38 : m_config(
std::move(config))
42CoreMidiBackend::~CoreMidiBackend()
44 if (m_initialized.load()) {
49bool CoreMidiBackend::initialize()
51 if (m_initialized.load()) {
53 "CoreMidiBackend already initialized");
57 OSStatus status = MIDIClientCreate(
59 &CoreMidiBackend::midi_notify_callback,
63 if (status != noErr) {
65 "Failed to create CoreMIDI client (status={})",
66 static_cast<int>(status));
70 m_initialized.store(
true);
74 create_virtual_port_if_enabled();
77 "CoreMidiBackend initialized with {} port(s)",
78 m_enumerated_devices.size());
83void CoreMidiBackend::start()
85 if (!m_initialized.load()) {
87 "Cannot start CoreMidiBackend: not initialized");
91 if (m_running.load()) {
93 "CoreMidiBackend already running");
97 if (m_config.auto_open_inputs) {
98 std::vector<uint32_t> to_open;
101 std::lock_guard lock(m_devices_mutex);
103 for (
const auto& [
id, info] : m_enumerated_devices) {
104 to_open.push_back(
id);
108 for (uint32_t
id : to_open) {
113 m_running.store(
true);
116 "CoreMidiBackend started with {} open port(s)",
117 get_open_devices().size());
120void CoreMidiBackend::stop()
122 if (!m_running.load())
125 std::vector<uint32_t> to_close;
128 std::lock_guard lock(m_devices_mutex);
130 for (
const auto& [
id, state] : m_open_devices) {
131 to_close.push_back(
id);
135 for (uint32_t
id : to_close) {
139 m_running.store(
false);
141 MF_INFO(C, X,
"CoreMidiBackend stopped");
144void CoreMidiBackend::shutdown()
146 if (!m_initialized.load())
151 if (m_virtual_destination) {
152 MIDIEndpointDispose(m_virtual_destination);
153 m_virtual_destination = 0;
157 std::lock_guard lock(m_devices_mutex);
158 m_open_devices.clear();
159 m_enumerated_devices.clear();
163 MIDIClientDispose(m_client);
167 m_initialized.store(
false);
169 MF_INFO(C, X,
"CoreMidiBackend shutdown complete");
172std::vector<InputDeviceInfo> CoreMidiBackend::get_devices()
const
174 std::lock_guard lock(m_devices_mutex);
176 std::vector<InputDeviceInfo> result;
177 result.reserve(m_enumerated_devices.size());
179 for (
const auto& [
id, info] : m_enumerated_devices) {
180 result.push_back(info);
186size_t CoreMidiBackend::refresh_devices()
188 std::vector<InputDeviceInfo> newly_added;
189 std::vector<InputDeviceInfo> removed_devices;
191 const ItemCount
count = MIDIGetNumberOfSources();
193 std::unordered_set<MIDIUniqueID> seen;
196 std::lock_guard lock(m_devices_mutex);
198 for (ItemCount i = 0; i <
count; ++i) {
199 MIDIEndpointRef endpoint = MIDIGetSource(i);
203 MIDIUniqueID unique_id = 0;
204 if (MIDIObjectGetIntegerProperty(endpoint, kMIDIPropertyUniqueID, &unique_id) != noErr)
207 seen.insert(unique_id);
209 CFStringRef name_ref =
nullptr;
210 MIDIObjectGetStringProperty(endpoint, kMIDIPropertyName, &name_ref);
211 std::string cname = cfstring_to_string(name_ref);
216 if (!port_matches_filter(cname))
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();
222 MIDIPortInfo info {};
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);
233 m_enumerated_devices[dev_id] = info;
235 newly_added.push_back(info);
238 for (
auto it = m_enumerated_devices.begin();
239 it != m_enumerated_devices.end();) {
241 if (!seen.contains(it->second.unique_id)) {
243 removed_devices.push_back(it->second);
245 auto open_it = m_open_devices.find(it->first);
247 if (open_it != m_open_devices.end()) {
248 auto state = std::move(open_it->second);
250 destroy_open_port(*state);
252 m_open_devices.erase(open_it);
255 it = m_enumerated_devices.erase(it);
262 for (
const auto& info : newly_added) {
263 MF_INFO(C, X,
"MIDI port found: '{}'", info.name);
264 notify_device_change(info,
true);
267 for (
const auto& info : removed_devices) {
268 MF_INFO(C, X,
"MIDI port removed: '{}'", info.name);
269 notify_device_change(info,
false);
272 std::lock_guard lock(m_devices_mutex);
273 return m_enumerated_devices.size();
276bool CoreMidiBackend::open_device(uint32_t device_id)
278 std::lock_guard lock(m_devices_mutex);
280 if (m_open_devices.find(device_id) != m_open_devices.end()) {
284 auto it = m_enumerated_devices.find(device_id);
286 if (it == m_enumerated_devices.end()) {
290 auto state = std::make_shared<MIDIPortState>();
292 state->info = it->second;
293 state->device_id = device_id;
296 std::lock_guard cb_lock(m_callback_mutex);
297 state->input_callback = m_input_callback;
300 OSStatus status = MIDIInputPortCreate(
302 CFSTR(
"MayaFlux Input"),
303 &CoreMidiBackend::midi_read_callback,
307 if (status != noErr) {
311 status = MIDIPortConnectSource(
313 state->info.endpoint,
316 if (status != noErr) {
317 MIDIPortDispose(state->input_port);
318 state->input_port = 0;
322 state->active.store(
true);
324 m_open_devices.insert_or_assign(device_id, state);
327 "Opened MIDI port {}: '{}'",
334void CoreMidiBackend::close_device(uint32_t device_id)
336 std::shared_ptr<MIDIPortState> state;
339 std::lock_guard lock(m_devices_mutex);
341 auto it = m_open_devices.find(device_id);
343 if (it == m_open_devices.end()) {
347 state = std::move(it->second);
349 m_open_devices.erase(it);
352 destroy_open_port(*state);
355 "Closed MIDI port {}: '{}'",
360void CoreMidiBackend::destroy_open_port(MIDIPortState& state)
362 state.active.store(
false);
364 if (state.input_port) {
365 MIDIPortDisconnectSource(
367 state.info.endpoint);
369 MIDIPortDispose(state.input_port);
371 state.input_port = 0;
375bool CoreMidiBackend::is_device_open(uint32_t device_id)
const
377 std::lock_guard lock(m_devices_mutex);
379 return m_open_devices.find(device_id)
380 != m_open_devices.end();
383std::vector<uint32_t> CoreMidiBackend::get_open_devices()
const
385 std::lock_guard lock(m_devices_mutex);
387 std::vector<uint32_t> result;
388 result.reserve(m_open_devices.size());
390 for (
const auto& [
id, state] : m_open_devices) {
391 result.push_back(
id);
397void CoreMidiBackend::set_input_callback(InputCallback callback)
399 std::lock_guard lock(m_callback_mutex);
401 m_input_callback = std::move(callback);
404void CoreMidiBackend::set_device_callback(DeviceCallback callback)
406 std::lock_guard lock(m_callback_mutex);
407 m_device_callback = std::move(callback);
410bool CoreMidiBackend::port_matches_filter(
const std::string& port_name)
const
412 if (m_config.input_port_filters.empty())
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;
422uint32_t CoreMidiBackend::find_or_assign_device_id(
423 MIDIUniqueID unique_id)
425 for (
const auto& [
id, info] : m_enumerated_devices) {
426 if (info.unique_id == unique_id)
430 return m_next_device_id++;
433void CoreMidiBackend::create_virtual_port_if_enabled()
435 if (!m_config.enable_virtual_port)
438 if (m_virtual_destination)
441 CFStringRef name = CFStringCreateWithCString(
443 m_config.virtual_port_name.c_str(),
444 kCFStringEncodingUTF8);
448 "Failed to create CoreFoundation string for virtual MIDI destination");
452 OSStatus status = MIDIDestinationCreate(
455 &CoreMidiBackend::virtual_destination_callback,
457 &m_virtual_destination);
461 if (status != noErr) {
463 "Failed to create virtual MIDI destination '{}' (status={})",
464 m_config.virtual_port_name,
465 static_cast<int>(status));
470 "Created virtual MIDI destination '{}'",
471 m_config.virtual_port_name);
474void CoreMidiBackend::midi_read_callback(
475 const MIDIPacketList* packet_list,
476 void* read_proc_ref_con,
479 auto* state =
static_cast<MIDIPortState*
>(read_proc_ref_con);
484 if (!state->active.load())
487 if (!state->input_callback)
490 const MIDIPacket* packet = &packet_list->packet[0];
492 for (UInt32 i = 0; i < packet_list->numPackets; ++i) {
493 if (packet->length == 0) {
494 packet = MIDIPacketNext(packet);
498 if (packet->length > 3) {
500 std::vector<uint8_t> bytes(
502 packet->data + packet->length);
504 state->input_callback(
505 InputValue::make_bytes(
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;
515 state->input_callback(
516 InputValue::make_midi(
523 packet = MIDIPacketNext(packet);
527void CoreMidiBackend::notify_device_change(
528 const InputDeviceInfo& info,
531 std::lock_guard lock(m_callback_mutex);
533 if (m_device_callback) {
534 m_device_callback(info, connected);
538void CoreMidiBackend::midi_notify_callback(
539 const MIDINotification* notification,
542 auto* self =
static_cast<CoreMidiBackend*
>(ref_con);
544 if (!self || !notification) {
548 switch (notification->messageID) {
550 case kMIDIMsgObjectAdded: {
551 const auto* msg =
reinterpret_cast<const MIDIObjectAddRemoveNotification*
>(notification);
554 Journal::Component::Core,
555 Journal::Context::InputBackend,
556 "CoreMIDI object added (type={})",
557 static_cast<int>(msg->childType));
559 self->refresh_devices();
563 case kMIDIMsgObjectRemoved: {
564 const auto* msg =
reinterpret_cast<const MIDIObjectAddRemoveNotification*
>(notification);
567 Journal::Component::Core,
568 Journal::Context::InputBackend,
569 "CoreMIDI object removed (type={})",
570 static_cast<int>(msg->childType));
572 self->refresh_devices();
576 case kMIDIMsgPropertyChanged: {
577 const auto* msg =
reinterpret_cast<const MIDIObjectPropertyChangeNotification*
>(notification);
579 std::string property_name;
581 if (msg->propertyName) {
582 property_name = cfstring_to_string(msg->propertyName);
586 Journal::Component::Core,
587 Journal::Context::InputBackend,
588 "CoreMIDI property changed: {}",
589 property_name.empty() ?
"<unknown>" : property_name);
591 self->refresh_devices();
595 case kMIDIMsgSetupChanged:
597 Journal::Component::Core,
598 Journal::Context::InputBackend,
599 "CoreMIDI setup changed");
601 self->refresh_devices();
604 case kMIDIMsgIOError: {
605 const auto* msg =
reinterpret_cast<const MIDIIOErrorNotification*
>(notification);
608 Journal::Component::Core,
609 Journal::Context::InputBackend,
610 "CoreMIDI I/O error on device {} (error={})",
619 Journal::Component::Core,
620 Journal::Context::InputBackend,
621 "Unhandled CoreMIDI notification {}",
622 static_cast<int>(notification->messageID));
627void CoreMidiBackend::virtual_destination_callback(
628 const MIDIPacketList* packet_list,
629 void* read_proc_ref_con,
632 auto* self =
static_cast<CoreMidiBackend*
>(read_proc_ref_con);
640 std::lock_guard lock(self->m_callback_mutex);
641 callback = self->m_input_callback;
647 const MIDIPacket* packet = &packet_list->packet[0];
649 for (UInt32 i = 0; i < packet_list->numPackets; ++i) {
651 if (packet->length == 0) {
652 packet = MIDIPacketNext(packet);
656 if (packet->length > 3 && self->m_config.enable_sysex) {
658 std::vector<uint8_t> bytes(
660 packet->data + packet->length);
663 InputValue::make_bytes(
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;
674 InputValue::make_midi(
681 packet = MIDIPacketNext(packet);
685std::string CoreMidiBackend::get_version()
const
#define MF_INFO(comp, ctx,...)
#define MF_ERROR(comp, ctx,...)
#define MF_WARN(comp, ctx,...)
#define MF_DEBUG(comp, ctx,...)
std::function< void(const InputValue &)> InputCallback
Callback signature for input events.
@ InputBackend
Input device backend (HID, MIDI, OSC)
@ Core
Core engine, backend, subsystems.
void stop()
Stop all Portal::Graphics operations.