diff --git a/Makefile.common b/Makefile.common index d7a8b97799d0..30e385a40f95 100644 --- a/Makefile.common +++ b/Makefile.common @@ -982,6 +982,12 @@ ifeq ($(HAVE_WINMM), 1) LIBS += -lwinmm endif +ifeq ($(HAVE_COREMIDI), 1) + OBJ += midi/drivers/coremidi.o + DEFINES += -DHAVE_COREMIDI + LIBS += -framework CoreMIDI +endif + # Audio Resamplers ifeq ($(HAVE_NEON),1) diff --git a/configuration.c b/configuration.c index 3b265aad9cad..6c83cd934d89 100644 --- a/configuration.c +++ b/configuration.c @@ -285,6 +285,7 @@ enum midi_driver_enum { MIDI_WINMM = RECORD_NULL + 1, MIDI_ALSA, + MIDI_COREMIDI, MIDI_NULL }; @@ -597,6 +598,8 @@ static const enum record_driver_enum RECORD_DEFAULT_DRIVER = RECORD_WAV; #ifdef HAVE_WINMM static const enum midi_driver_enum MIDI_DEFAULT_DRIVER = MIDI_WINMM; +#elif defined(HAVE_COREMIDI) +static const enum midi_driver_enum MIDI_DEFAULT_DRIVER = MIDI_COREMIDI; #elif defined(HAVE_ALSA) && !defined(HAVE_HAKCHI) && !defined(HAVE_SEGAM) && !defined(DINGUX) static const enum midi_driver_enum MIDI_DEFAULT_DRIVER = MIDI_ALSA; #else @@ -1455,6 +1458,8 @@ const char *config_get_default_midi(void) return "winmm"; case MIDI_ALSA: return "alsa"; + case MIDI_COREMIDI: + return "coremidi"; case MIDI_NULL: break; } diff --git a/griffin/griffin.c b/griffin/griffin.c index 22fbd7dca3bf..9fd5d0820fcf 100644 --- a/griffin/griffin.c +++ b/griffin/griffin.c @@ -961,6 +961,10 @@ MIDI #include "../midi/drivers/winmm_midi.c" #endif +#ifdef HAVE_COREMIDI +#include "../midi/drivers/coremidi.c" +#endif + /*============================================================ DRIVERS ============================================================ */ diff --git a/midi/drivers/coremidi.c b/midi/drivers/coremidi.c new file mode 100644 index 000000000000..7907666807c8 --- /dev/null +++ b/midi/drivers/coremidi.c @@ -0,0 +1,415 @@ +/* RetroArch - A frontend for libretro. + * Copyright (C) 2025 The RetroArch team + * + * RetroArch is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with RetroArch. + * If not, see . + */ + +#include +#include +#include +#include + +#include "../midi_driver.h" +#include "../../verbosity.h" + +#define CORE_MIDI_QUEUE_SIZE 1024 + +typedef struct { + midi_event_t events[CORE_MIDI_QUEUE_SIZE]; ///< Event buffer + int read_index; ///< Current read position + int write_index; ///< Current write position +} coremidi_queue_t; + +typedef struct { + MIDIClientRef client; ///< CoreMIDI client + MIDIPortRef input_port; ///< Input port for receiving MIDI data + MIDIPortRef output_port; ///< Output port for sending MIDI data + MIDIEndpointRef input_endpoint; ///< Selected input endpoint + MIDIEndpointRef output_endpoint; ///< Selected output endpoint + coremidi_queue_t input_queue; ///< Queue for incoming MIDI events +} coremidi_t; + +/// Write to the queue +static bool coremidi_queue_write(coremidi_queue_t *q, const midi_event_t *ev) { + int next_write = (q->write_index + 1) % CORE_MIDI_QUEUE_SIZE; + if (next_write == q->read_index) // Queue full + return false; + + memcpy(&q->events[q->write_index], ev, sizeof(*ev)); + q->write_index = next_write; + return true; +} + +/// MIDIReadProc callback function +static void midi_read_callback(const MIDIPacketList *pktlist, void *readProcRefCon, void *srcConnRefCon) { + coremidi_t *d = (coremidi_t *)readProcRefCon; + const MIDIPacket *packet = &pktlist->packet[0]; + midi_event_t event; + uint32_t i; + + for (i = 0; i < pktlist->numPackets; i++) { + const uint8_t *data = packet->data; + size_t length = packet->length; + const MIDITimeStamp timestamp = packet->timeStamp; + + while (length > 0) { + size_t msg_size = midi_driver_get_event_size(data[0]); + if (msg_size == 0 || msg_size > length) + break; + + event.data = (uint8_t *)data; + event.data_size = msg_size; + event.delta_time = (uint32_t)timestamp; + + // Add to queue + coremidi_queue_write(&d->input_queue, &event); + + data += msg_size; + length -= msg_size; + } + + packet = MIDIPacketNext(packet); + } +} + +/// Initialize the CoreMIDI client and ports +static void *coremidi_init(const char *input, const char *output) { + coremidi_t *d = (coremidi_t *)calloc(1, sizeof(coremidi_t)); + if (!d) { + RARCH_ERR("[MIDI]: Out of memory.\n"); + return NULL; + } + + OSStatus err = MIDIClientCreate(CFSTR("RetroArch MIDI Client"), NULL, NULL, &d->client); + if (err != noErr) { + RARCH_ERR("[MIDI]: MIDIClientCreate failed: %d\n", err); + free(d); + return NULL; + } else { + RARCH_LOG("[MIDI]: CoreMIDI client created successfully\n"); + } + + // Create input port if specified + if (input) { + err = MIDIInputPortCreate(d->client, CFSTR("Input Port"), midi_read_callback, d, &d->input_port); + if (err != noErr) { + RARCH_ERR("[MIDI]: MIDIInputPortCreate failed: %d\n", err); + MIDIClientDispose(d->client); + free(d); + return NULL; + } + } else { + RARCH_LOG("[MIDI]: CoreMIDI input port created successfully\n"); + } + + // Create output port if specified + if (output) { + err = MIDIOutputPortCreate(d->client, CFSTR("Output Port"), &d->output_port); + if (err != noErr) { + RARCH_ERR("[MIDI]: MIDIOutputPortCreate failed: %d\n", err); + if (d->input_port) + MIDIPortDispose(d->input_port); + MIDIClientDispose(d->client); + free(d); + return NULL; + } + } else { + RARCH_LOG("[MIDI]: CoreMIDI output port created successfully\n"); + } + + return d; +} + +/// Get available input devices +static bool coremidi_get_avail_inputs(struct string_list *inputs) { + ItemCount count = MIDIGetNumberOfSources(); + union string_list_elem_attr attr = {0}; + + for (ItemCount i = 0; i < count; i++) { + MIDIEndpointRef endpoint = MIDIGetSource(i); + CFStringRef name; + char buf[256]; + + OSStatus err = MIDIObjectGetStringProperty(endpoint, kMIDIPropertyDisplayName, &name); + if (err == noErr) { + if (CFStringGetCString(name, buf, sizeof(buf), kCFStringEncodingUTF8)) { + if (!string_list_append(inputs, buf, attr)) { + RARCH_ERR("[MIDI]: Failed to append input device to list: %s\n", buf); + CFRelease(name); + return false; + } else { + RARCH_LOG("[MIDI]: Input device added to list: %s\n", buf); + } + } + CFRelease(name); + } else { + RARCH_WARN("[MIDI]: Failed to get display name for input device %d: %d\n", i, err); + } + } + + return true; +} + +/// Get available output devices +static bool coremidi_get_avail_outputs(struct string_list *outputs) { + ItemCount count = MIDIGetNumberOfDestinations(); + union string_list_elem_attr attr = {0}; + + for (ItemCount i = 0; i < count; i++) { + MIDIEndpointRef endpoint = MIDIGetDestination(i); + CFStringRef name; + char buf[256]; + + OSStatus err = MIDIObjectGetStringProperty(endpoint, kMIDIPropertyDisplayName, &name); + if (err == noErr) { + if (CFStringGetCString(name, buf, sizeof(buf), kCFStringEncodingUTF8)) { + if (!string_list_append(outputs, buf, attr)) { + RARCH_ERR("[MIDI]: Failed to append output device to list: %s\n", buf); + CFRelease(name); + return false; + } else { + RARCH_LOG("[MIDI]: Output device added to list: %s\n", buf); + } + } + CFRelease(name); + } else { + RARCH_WARN("[MIDI]: Failed to get display name for output device %d: %d\n", i, err); + } + } + + return true; +} + +/// Set the input device +static bool coremidi_set_input(void *p, const char *input) { + coremidi_t *d = (coremidi_t *)p; + if (!d || !d->input_port) { + RARCH_WARN("[MIDI]: Input port not initialized\n"); + return false; + } + + // Disconnect current input endpoint + if (d->input_endpoint) { + RARCH_LOG("[MIDI]: Disconnecting current input endpoint\n"); + MIDIPortDisconnectSource(d->input_port, d->input_endpoint); + d->input_endpoint = 0; + } + + // If input is NULL or "Off", just return success + if (!input || string_is_equal(input, "Off")) { + RARCH_LOG("[MIDI]: Input set to Off\n"); + return true; + } + + // Find the input endpoint by name + ItemCount count = MIDIGetNumberOfSources(); + for (ItemCount i = 0; i < count; i++) { + MIDIEndpointRef endpoint = MIDIGetSource(i); + CFStringRef name; + char buf[256]; + + OSStatus err = MIDIObjectGetStringProperty(endpoint, kMIDIPropertyDisplayName, &name); + if (err == noErr) { + if (CFStringGetCString(name, buf, sizeof(buf), kCFStringEncodingUTF8) && string_is_equal(input, buf)) { + err = MIDIPortConnectSource(d->input_port, endpoint, NULL); + CFRelease(name); + if (err == noErr) { + d->input_endpoint = endpoint; + RARCH_LOG("[MIDI]: Input endpoint set to %s\n", buf); + return true; + } else { + RARCH_WARN("[MIDI]: Failed to connect input endpoint: %d\n", err); + return false; + } + } + CFRelease(name); + } + } + + RARCH_WARN("[MIDI]: Input device not found: %s\n", input); + return false; +} + +/// Set the output device +static bool coremidi_set_output(void *p, const char *output) { + coremidi_t *d = (coremidi_t *)p; + if (!d || !d->output_port) { + RARCH_WARN("[MIDI]: Output port not initialized\n"); + return false; + } + + // If output is NULL or "Off", just return success + if (!output || string_is_equal(output, "Off")) { + RARCH_LOG("[MIDI]: Output set to Off\n"); + d->output_endpoint = 0; + return true; + } + + // Find the output endpoint by name + ItemCount count = MIDIGetNumberOfDestinations(); + for (ItemCount i = 0; i < count; i++) { + MIDIEndpointRef endpoint = MIDIGetDestination(i); + CFStringRef name; + char buf[256]; + + OSStatus err = MIDIObjectGetStringProperty(endpoint, kMIDIPropertyDisplayName, &name); + if (err == noErr) { + if (CFStringGetCString(name, buf, sizeof(buf), kCFStringEncodingUTF8) && string_is_equal(output, buf)) { + d->output_endpoint = endpoint; + RARCH_LOG("[MIDI]: Output endpoint set to %s\n", buf); + CFRelease(name); + return true; + } + CFRelease(name); + } + } + + RARCH_WARN("[MIDI]: Output device not found: %s\n", output); + return false; +} + +/// Read from the queue +static bool coremidi_queue_read(coremidi_queue_t *q, midi_event_t *ev) { + if (q->read_index == q->write_index) // Queue empty + return false; + + memcpy(ev, &q->events[q->read_index], sizeof(*ev)); + q->read_index = (q->read_index + 1) % CORE_MIDI_QUEUE_SIZE; + return true; +} + +/// Read a MIDI event +static bool coremidi_read(void *p, midi_event_t *event) { + coremidi_t *d = (coremidi_t *)p; + + if (!d || !event) { + RARCH_WARN("[MIDI]: Invalid parameters in coremidi_read\n"); + return false; + } + + if (!d->input_port || !d->input_endpoint) { + RARCH_WARN("[MIDI]: Input not configured\n"); + return false; + } + + int result = coremidi_queue_read(&d->input_queue, event); +// #if DEBUG + RARCH_LOG("[MIDI]: Input queue read result: %d\n", result); +// #endif + return result; +} + +/// Write a MIDI event +static bool coremidi_write(void *p, const midi_event_t *event) { + coremidi_t *d = (coremidi_t *)p; + + if (!d || !event) { + RARCH_WARN("[MIDI]: Invalid parameters in coremidi_write\n"); + return false; + } + + if (!d->output_port || !d->output_endpoint) { + RARCH_WARN("[MIDI]: Output not configured\n"); + return false; + } + + // Validate event data + if (!event->data || event->data_size == 0 || event->data_size > 65535) { + RARCH_WARN("[MIDI]: Invalid event data in coremidi_write\n"); + return false; + } + + // Create a MIDIPacketList with sufficient buffer size + char packetListBuffer[sizeof(MIDIPacketList) + sizeof(MIDIPacket) + event->data_size]; + MIDIPacketList *packetList = (MIDIPacketList *)packetListBuffer; + MIDIPacket *packet = MIDIPacketListInit(packetList); + + // Add the event to the packet list + packet = MIDIPacketListAdd(packetList, sizeof(packetListBuffer), packet, 0, event->data_size, event->data); + if (!packet) { + RARCH_WARN("[MIDI]: Failed to create MIDIPacketList\n"); + return false; + } + + // Send the packet + OSStatus err = MIDISend(d->output_port, d->output_endpoint, packetList); + if (err != noErr) { + RARCH_WARN("[MIDI]: MIDISend failed: %d\n", err); + return false; + } + + return true; +} + +/// Flush the output buffer +static bool coremidi_flush(void *p) { + coremidi_t *d = (coremidi_t *)p; + + if (!d) { + RARCH_WARN("[MIDI]: Invalid parameters in coremidi_flush\n"); + return false; + } + + if (!d->output_port || !d->output_endpoint) { + RARCH_WARN("[MIDI]: Output not configured\n"); + return false; + } + + // CoreMIDI doesn't require explicit flushing, so we just return success + // RARCH_LOG("[MIDI]: Output buffer flushed\n"); + return true; +} + +/// Free resources and cleanup +static void coremidi_free(void *p) { + coremidi_t *d = (coremidi_t *)p; + + if (!d) { + RARCH_WARN("[MIDI]: Invalid parameters in coremidi_free\n"); + return; + } + + // Clean up MIDI resources + if (d->input_port) { + RARCH_LOG("[MIDI]: Disconnecting input port\n"); + MIDIPortDisconnectSource(d->input_port, d->input_endpoint); + MIDIPortDispose(d->input_port); + } + + if (d->output_port) { + RARCH_LOG("[MIDI]: Disconnecting output port\n"); + MIDIPortDisconnectSource(d->output_port, d->output_endpoint); + MIDIPortDispose(d->output_port); + } + + if (d->client) { + RARCH_LOG("[MIDI]: Disposing of CoreMIDI client\n"); + MIDIClientDispose(d->client); + } + + // Free the driver instance + free(d); +} + +/// CoreMIDI driver API +midi_driver_t midi_coremidi = { + .ident = "coremidi", + .init = coremidi_init, + .free = coremidi_free, + .set_input = coremidi_set_input, + .set_output = coremidi_set_output, + .read = coremidi_read, + .write = coremidi_write, + .flush = coremidi_flush, + .get_avail_inputs = coremidi_get_avail_inputs, + .get_avail_outputs = coremidi_get_avail_outputs, +}; diff --git a/midi_driver.h b/midi_driver.h index 6f22182afbf5..faae8e1a2c5f 100644 --- a/midi_driver.h +++ b/midi_driver.h @@ -236,6 +236,7 @@ bool midi_driver_set_all_sounds_off(void); extern midi_driver_t midi_winmm; extern midi_driver_t midi_alsa; +extern midi_driver_t midi_coremidi; extern midi_driver_t *midi_drivers[]; diff --git a/retroarch.c b/retroarch.c index 0113e7dc48e6..9bc5644672ef 100644 --- a/retroarch.c +++ b/retroarch.c @@ -559,6 +559,9 @@ midi_driver_t *midi_drivers[] = { #endif #ifdef HAVE_WINMM &midi_winmm, +#endif +#ifdef HAVE_COREMIDI + &midi_coremidi, #endif &midi_null };