diff --git a/cmake/libremidi.sources.cmake b/cmake/libremidi.sources.cmake index 4fb1859c..a18c2851 100644 --- a/cmake/libremidi.sources.cmake +++ b/cmake/libremidi.sources.cmake @@ -44,6 +44,9 @@ target_sources(libremidi PRIVATE include/libremidi/backends/coremidi_ump/midi_in.hpp include/libremidi/backends/coremidi_ump/midi_out.hpp include/libremidi/backends/coremidi_ump/observer.hpp + include/libremidi/backends/coremidi_ump/endpoint_config.hpp + include/libremidi/backends/coremidi_ump/endpoint.hpp + include/libremidi/backends/coremidi_ump/endpoint_observer.hpp include/libremidi/backends/coremidi_ump.hpp include/libremidi/backends/dummy.hpp diff --git a/examples/midi2_interop.cpp b/examples/midi2_interop.cpp index c9f4c65f..e4e9282d 100644 --- a/examples/midi2_interop.cpp +++ b/examples/midi2_interop.cpp @@ -12,7 +12,9 @@ #include #include +#include #include +#include #include struct midi_ci_processor @@ -167,7 +169,7 @@ struct midi_ci_processor case end_of_file: std::cerr << "[sysex] end_of_file" << std::endl; break; - case wait: + case midi::universal_sysex::type::wait: std::cerr << "[sysex] wait" << std::endl; break; case cancel: @@ -356,7 +358,7 @@ try auto inquiry = midi::ci::make_discovery_inquiry(my_muid, id, 0x02, 512); processor.midiout.send_ump(inquiry); - sleep(2); + std::this_thread::sleep_for(std::chrono::seconds(2)); } char input; diff --git a/include/libremidi/backends/coremidi/helpers.hpp b/include/libremidi/backends/coremidi/helpers.hpp index 2d851341..a9afd00a 100644 --- a/include/libremidi/backends/coremidi/helpers.hpp +++ b/include/libremidi/backends/coremidi/helpers.hpp @@ -8,6 +8,12 @@ #include #include +// Hack: MacTypes.h defines nil as nullptr even in C++ translation +// units, which conflicts with libraries that use nil as an identifier (e.g. msgpack). +#ifdef nil + #undef nil +#endif + #include #include diff --git a/include/libremidi/backends/coremidi_ump.hpp b/include/libremidi/backends/coremidi_ump.hpp index f5cdd483..9adca508 100644 --- a/include/libremidi/backends/coremidi_ump.hpp +++ b/include/libremidi/backends/coremidi_ump.hpp @@ -2,6 +2,8 @@ #include #include #include +#include +#include #include @@ -16,15 +18,15 @@ struct backend using midi_out_configuration = coremidi_ump::output_configuration; using midi_observer_configuration = coremidi_ump::observer_configuration; - using midi_endpoint = void; - using midi_endpoint_observer = void; - using midi_endpoint_configuration = void; - using midi_endpoint_observer_configuration = void; + using midi_endpoint = coremidi_ump::endpoint_impl; + using midi_endpoint_observer = coremidi_ump::endpoint_observer_impl; + using midi_endpoint_configuration = coremidi_ump::endpoint_api_configuration; + using midi_endpoint_observer_configuration = coremidi_ump::endpoint_observer_api_configuration; static const constexpr auto API = libremidi::API::COREMIDI_UMP; static const constexpr std::string_view name = "core_ump"; static const constexpr std::string_view display_name = "CoreMIDI UMP"; - static constexpr inline bool available() noexcept { return true; /* todo? */ } + static constexpr inline bool available() noexcept { return true; } }; } diff --git a/include/libremidi/backends/coremidi_ump/endpoint.hpp b/include/libremidi/backends/coremidi_ump/endpoint.hpp new file mode 100644 index 00000000..dfd21654 --- /dev/null +++ b/include/libremidi/backends/coremidi_ump/endpoint.hpp @@ -0,0 +1,345 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +#if __MAC_OS_X_VERSION_MIN_REQUIRED < 140000 +#define kMIDIPropertyUMPCanTransmitGroupless CFSTR("ump endpoint") +#define kMIDIPropertyUMPActiveGroupBitmap CFSTR("active group bitmap") +#endif +#if __MAC_OS_X_VERSION_MIN_REQUIRED < 150000 +#define kMIDIPropertyAssociatedEndpoint CFSTR("associated endpoint") +#endif + +NAMESPACE_LIBREMIDI::coremidi_ump +{ + +class endpoint_impl final + : public ump_endpoint_api + , public error_handler +{ +public: + struct + : libremidi::remote_ump_endpoint_configuration + , coremidi_ump::endpoint_api_configuration + { + } configuration; + + endpoint_impl( + libremidi::remote_ump_endpoint_configuration&& conf, + coremidi_ump::endpoint_api_configuration&& apiconf) + : configuration{std::move(conf), std::move(apiconf)} + { + if (configuration.context) + { + m_client = *configuration.context; + } + else + { + auto result = MIDIClientCreate( + toCFString(configuration.client_name).get(), nullptr, nullptr, &m_client); + if (result != noErr) + { + libremidi_handle_error( + this->configuration, "error creating MIDI client object: " + std::to_string(result)); + client_open_ = std::errc::io_error; + return; + } + m_owns_client = true; + } + + client_open_ = stdx::error{}; + } + + ~endpoint_impl() override + { + close(); + + if (m_owns_client && m_client) + MIDIClientDispose(m_client); + } + + libremidi::API get_current_api() const noexcept override + { + return libremidi::API::COREMIDI_UMP; + } + + stdx::error open(const ump_endpoint_info& endpoint, std::string_view /*local_name*/) override + { + CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, false); + + const auto* id_str = libremidi_variant_alias::get_if(&endpoint.endpoint_id); + if (!id_str) + return std::errc::invalid_argument; + + MIDIEndpointRef source_ref = 0; + MIDIEndpointRef dest_ref = 0; + + auto colon_pos = id_str->find(':'); + if (colon_pos == std::string::npos) + return std::errc::invalid_argument; + + try + { + source_ref = static_cast(std::stoul(id_str->substr(0, colon_pos))); + dest_ref = static_cast(std::stoul(id_str->substr(colon_pos + 1))); + } + catch (...) + { + return std::errc::invalid_argument; + } + + m_protocol = kMIDIProtocol_2_0; + if (endpoint.active_protocol == midi_protocol::midi1) + m_protocol = kMIDIProtocol_1_0; + + if (source_ref && (configuration.on_message || configuration.on_raw_data)) + { + MIDIPortRef in_port; + OSStatus result = MIDIInputPortCreateWithProtocol( + m_client, toCFString("Input").get(), m_protocol, &in_port, + ^(const MIDIEventList* evtlist, void* __nullable /*srcConnRefCon*/) { + this->midiInputCallback(evtlist); + }); + + if (result != noErr) + { + libremidi_handle_error(this->configuration, "error creating macOS MIDI input port."); + return std::errc::io_error; + } + + if (result = MIDIPortConnectSource(in_port, source_ref, nullptr); result != noErr) + { + MIDIPortDispose(in_port); + libremidi_handle_error(this->configuration, "error connecting macOS MIDI input port."); + return std::errc::io_error; + } + + m_input_port = in_port; + } + + if (dest_ref) + { + MIDIPortRef out_port; + OSStatus result = MIDIOutputPortCreate(m_client, toCFString("Output").get(), &out_port); + if (result != noErr) + { + if (m_input_port) MIDIPortDispose(m_input_port); + m_input_port = 0; + libremidi_handle_error(this->configuration, "error creating macOS MIDI output port."); + return std::errc::io_error; + } + m_output_port = out_port; + m_destinationId = dest_ref; + } + + connected_endpoint_ = endpoint; + active_protocol_ = endpoint.active_protocol; + port_open_ = true; + connected_ = true; + + return stdx::error{}; + } + + stdx::error open_virtual(std::string_view port_name) override + { + auto cf_name = toCFString(port_name); + + OSStatus result = MIDISourceCreateWithProtocol( + m_client, cf_name.get(), kMIDIProtocol_2_0, &m_virtual_source); + if (result != noErr) + { + libremidi_handle_error(this->configuration, "error creating virtual MIDI source."); + return std::errc::io_error; + } + + MIDIObjectSetIntegerProperty(m_virtual_source, kMIDIPropertyUMPCanTransmitGroupless, 1); + MIDIObjectSetIntegerProperty(m_virtual_source, kMIDIPropertyUMPActiveGroupBitmap, 0xFFFF); + + result = MIDIDestinationCreateWithProtocol( + m_client, cf_name.get(), kMIDIProtocol_2_0, &m_virtual_destination, + ^(const MIDIEventList* evtlist, void*) { + this->midiInputCallback(evtlist); + }); + + if (result != noErr) + { + MIDIEndpointDispose(m_virtual_source); + m_virtual_source = 0; + libremidi_handle_error(this->configuration, "error creating virtual MIDI destination."); + return std::errc::io_error; + } + + MIDIObjectSetIntegerProperty(m_virtual_destination, kMIDIPropertyUMPCanTransmitGroupless, 1); + MIDIObjectSetIntegerProperty(m_virtual_destination, kMIDIPropertyUMPActiveGroupBitmap, 0xFFFF); + + // Associate source and destination so CoreMIDI treats them as one endpoint + SInt32 srcUID = 0, dstUID = 0; + MIDIObjectGetIntegerProperty(m_virtual_source, kMIDIPropertyUniqueID, &srcUID); + MIDIObjectGetIntegerProperty(m_virtual_destination, kMIDIPropertyUniqueID, &dstUID); + if (srcUID && dstUID) + { + MIDIObjectSetIntegerProperty(m_virtual_source, kMIDIPropertyAssociatedEndpoint, dstUID); + MIDIObjectSetIntegerProperty(m_virtual_destination, kMIDIPropertyAssociatedEndpoint, srcUID); + } + + active_protocol_ = midi_protocol::midi2; + port_open_ = true; + connected_ = true; + + return stdx::error{}; + } + + stdx::error close() override + { + if (m_virtual_source) + { + MIDIEndpointDispose(m_virtual_source); + m_virtual_source = 0; + } + if (m_virtual_destination) + { + MIDIEndpointDispose(m_virtual_destination); + m_virtual_destination = 0; + } + if (m_input_port) + { + MIDIPortDispose(m_input_port); + m_input_port = 0; + } + if (m_output_port) + { + MIDIPortDispose(m_output_port); + m_output_port = 0; + } + m_destinationId = 0; + + port_open_ = false; + connected_ = false; + connected_endpoint_.reset(); + + return stdx::error{}; + } + + stdx::error send_ump(const uint32_t* ump_stream, std::size_t count) override + { + MIDIEventList* eventList = reinterpret_cast(m_eventListBuffer); + MIDIEventPacket* packet = MIDIEventListInit(eventList, m_protocol); + const MIDITimeStamp ts = LIBREMIDI_AUDIO_GET_CURRENT_HOST_TIME(); + + auto write_fun = [ts, &packet, &eventList](const uint32_t* ump, int bytes) -> std::errc { + packet = MIDIEventListAdd(eventList, event_list_max_size, packet, ts, bytes / 4, ump); + return packet ? std::errc{0} : std::errc::not_enough_memory; + }; + + auto realloc_fun = [this, &packet, &eventList] { + push_event_list(eventList); + packet = MIDIEventListInit(eventList, m_protocol); + }; + + segment_ump_stream(ump_stream, count, write_fun, realloc_fun); + return push_event_list(eventList); + } + + int64_t current_time() const noexcept override + { + return coremidi_data::time_in_nanos(LIBREMIDI_AUDIO_GET_CURRENT_HOST_TIME()); + } + + stdx::error push_event_list(MIDIEventList* eventList) + { + if (m_virtual_source) + { + if (MIDIReceivedEventList(m_virtual_source, eventList) != noErr) + { + libremidi_handle_warning(this->configuration, "error sending MIDI to virtual destinations."); + return std::errc::io_error; + } + } + + if (m_destinationId != 0) + { + if (MIDISendEventList(m_output_port, m_destinationId, eventList) != noErr) + { + libremidi_handle_warning(this->configuration, "error sending MIDI message to port."); + return std::errc::io_error; + } + } + + return stdx::error{}; + } + + void midiInputCallback(const MIDIEventList* list) + { + const MIDIEventPacket* packet = &list->packet[0]; + for (unsigned int i = 0; i < list->numPackets; ++i) + { + if (packet->wordCount > 0) + { + const int64_t timestamp = coremidi_data::time_in_nanos(packet->timeStamp); + dispatch_ump(packet->words, packet->wordCount, timestamp); + } + packet = MIDIEventPacketNext(packet); + } + } + + void dispatch_ump(const uint32_t* words, size_t max_words, int64_t timestamp) + { + if (configuration.on_raw_data) + configuration.on_raw_data({words, max_words}, timestamp); + + if (configuration.on_message) + { + size_t offset = 0; + while (offset < max_words) + { + ump msg; + msg.data[0] = words[offset]; + + uint8_t type = (msg.data[0] >> 28) & 0x0F; + size_t msg_words = 1; + switch (type) + { + case 0x0: case 0x1: case 0x2: case 0x6: case 0x7: + msg_words = 1; break; + case 0x3: case 0x4: case 0x8: case 0x9: case 0xA: + msg_words = 2; break; + case 0xB: case 0xC: + msg_words = 3; break; + case 0x5: case 0xD: case 0xE: case 0xF: + msg_words = 4; break; + } + + if (offset + msg_words > max_words) + break; + + for (size_t w = 1; w < msg_words && w < 4; ++w) + msg.data[w] = words[offset + w]; + + msg.timestamp = timestamp; + configuration.on_message(std::move(msg)); + + offset += msg_words; + } + } + } + + MIDIClientRef m_client{0}; + bool m_owns_client{false}; + MIDIProtocolID m_protocol{kMIDIProtocol_2_0}; + + MIDIPortRef m_input_port{0}; + MIDIPortRef m_output_port{0}; + MIDIEndpointRef m_destinationId{0}; + + MIDIEndpointRef m_virtual_source{0}; + MIDIEndpointRef m_virtual_destination{0}; + + static constexpr int event_list_max_size = 65535; + unsigned char m_eventListBuffer[sizeof(MIDIEventList) + event_list_max_size]; +}; + +} diff --git a/include/libremidi/backends/coremidi_ump/endpoint_config.hpp b/include/libremidi/backends/coremidi_ump/endpoint_config.hpp new file mode 100644 index 00000000..0cf9a264 --- /dev/null +++ b/include/libremidi/backends/coremidi_ump/endpoint_config.hpp @@ -0,0 +1,23 @@ +#pragma once +#include +#include + +NAMESPACE_LIBREMIDI::coremidi_ump +{ + +struct endpoint_api_configuration +{ + std::string client_name = "libremidi client"; + std::optional context{}; + + ump_callback on_message{}; + raw_ump_callback on_raw_data{}; +}; + +struct endpoint_observer_api_configuration +{ + std::string client_name = "libremidi observer"; + std::function on_create_context{}; +}; + +} diff --git a/include/libremidi/backends/coremidi_ump/endpoint_observer.hpp b/include/libremidi/backends/coremidi_ump/endpoint_observer.hpp new file mode 100644 index 00000000..6f3ebf26 --- /dev/null +++ b/include/libremidi/backends/coremidi_ump/endpoint_observer.hpp @@ -0,0 +1,194 @@ +#pragma once +#include +#include +#include +#include +#include + +#include + +NAMESPACE_LIBREMIDI::coremidi_ump +{ + +class endpoint_observer_impl final + : public ump_endpoint_observer_api + , public error_handler +{ +public: + struct + : libremidi::observer_configuration + , coremidi_ump::endpoint_observer_api_configuration + { + } configuration; + + endpoint_observer_impl( + libremidi::observer_configuration&& conf, + coremidi_ump::endpoint_observer_api_configuration&& apiconf) + : configuration{std::move(conf), std::move(apiconf)} + { + OSStatus result = MIDIClientCreate( + toCFString(configuration.client_name).get(), + +[](const MIDINotification* message, void* ctx) { + static_cast(ctx)->on_notify(message); + }, + this, &m_client); + + if (result != noErr) + { + valid_ = std::errc::io_error; + return; + } + + if (configuration.on_create_context) + configuration.on_create_context(m_client); + + enumerate_endpoints(); + valid_ = stdx::error{}; + } + + ~endpoint_observer_impl() override + { + if (m_client) + MIDIClientDispose(m_client); + } + + libremidi::API get_current_api() const noexcept override + { + return libremidi::API::COREMIDI_UMP; + } + + std::vector get_endpoints() const noexcept override + { + return cached_endpoints_; + } + + stdx::error refresh() noexcept override + { + CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, false); + cached_endpoints_.clear(); + enumerate_endpoints(); + return stdx::error{}; + } + +private: + void on_notify(const MIDINotification* message) + { + switch (message->messageID) + { + case kMIDIMsgObjectAdded: + case kMIDIMsgObjectRemoved: + refresh(); + break; + default: + break; + } + } + + void enumerate_endpoints() + { + std::map> endpoints; + + auto num_sources = MIDIGetNumberOfSources(); + for (ItemCount i = 0; i < num_sources; ++i) + { + MIDIEndpointRef ep = MIDIGetSource(i); + auto name = get_endpoint_name(ep); + if (!name.empty()) + endpoints[name].first = ep; + } + + auto num_dests = MIDIGetNumberOfDestinations(); + for (ItemCount i = 0; i < num_dests; ++i) + { + MIDIEndpointRef ep = MIDIGetDestination(i); + auto name = get_endpoint_name(ep); + if (!name.empty()) + endpoints[name].second = ep; + } + + for (auto& [name, eps] : endpoints) + { + if (!eps.first && !eps.second) + continue; + + ump_endpoint_info info; + info.api = libremidi::API::COREMIDI_UMP; + info.name = name; + info.display_name = name; + + bool is_virtual = is_virtual_endpoint(eps.first ? eps.first : eps.second); + info.transport = is_virtual + ? endpoint_transport_type::virtual_port + : endpoint_transport_type::unknown; + + SInt32 proto = 0; + MIDIEndpointRef ref = eps.second ? eps.second : eps.first; + if (MIDIObjectGetIntegerProperty(ref, kMIDIPropertyProtocolID, &proto) == noErr + && proto == kMIDIProtocol_2_0) + { + info.active_protocol = midi_protocol::midi2; + info.supported_protocols = midi_protocol::both; + } + else + { + info.active_protocol = midi_protocol::midi1; + info.supported_protocols = midi_protocol::midi1; + } + + info.endpoint_id = std::to_string(eps.first) + ":" + std::to_string(eps.second); + + function_block_info fb; + fb.block_id = 0; + fb.active = true; + fb.groups = {0, 16}; + fb.name = name; + + if (eps.first && eps.second) + fb.direction = function_block_direction::bidirectional; + else if (eps.first) + fb.direction = function_block_direction::output; + else + fb.direction = function_block_direction::input; + + fb.ui_hint = function_block_ui_hint::both; + info.function_blocks.push_back(std::move(fb)); + + cached_endpoints_.push_back(std::move(info)); + } + } + + static std::string get_endpoint_name(MIDIEndpointRef ep) + { + if (!ep) + return {}; + + CFStringRef name = nullptr; + if (MIDIObjectGetStringProperty(ep, kMIDIPropertyName, &name) != noErr || !name) + return {}; + + char buf[512] = {}; + CFStringGetCString(name, buf, sizeof(buf), kCFStringEncodingUTF8); + CFRelease(name); + return std::string(buf); + } + + static bool is_virtual_endpoint(MIDIEndpointRef ep) + { + if (!ep) + return false; + + CFStringRef model = nullptr; + MIDIObjectGetStringProperty(ep, kMIDIPropertyModel, &model); + if (model) + { + CFRelease(model); + return false; + } + return true; + } + + MIDIClientRef m_client{0}; + std::vector cached_endpoints_; +}; + +} diff --git a/include/libremidi/cmidi2.hpp b/include/libremidi/cmidi2.hpp index 57c80c76..e68758ba 100644 --- a/include/libremidi/cmidi2.hpp +++ b/include/libremidi/cmidi2.hpp @@ -359,6 +359,8 @@ LIBREMIDI_STATIC uint8_t cmidi2_ump_get_num_bytes(uint32_t data) case CMIDI2_MESSAGE_TYPE_SYSEX7: return 8; case CMIDI2_MESSAGE_TYPE_SYSEX8_MDS: + case CMIDI2_MESSAGE_TYPE_FLEX_DATA: + case CMIDI2_MESSAGE_TYPE_UMP_STREAM: return 16; } return 0xFF; /* wrong */ diff --git a/include/libremidi/configurations.hpp b/include/libremidi/configurations.hpp index d6b77571..9a0ae713 100644 --- a/include/libremidi/configurations.hpp +++ b/include/libremidi/configurations.hpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -69,7 +70,8 @@ using observer_api_configuration = libremidi_variant_alias::variant< // Configuration for the new bidirectional UMP endpoint API using endpoint_api_configuration = std::variant< unspecified_configuration, dummy_configuration, alsa_raw_ump::endpoint_api_configuration, - alsa_seq_ump::endpoint_api_configuration, libremidi::API>; + alsa_seq_ump::endpoint_api_configuration, coremidi_ump::endpoint_api_configuration, + libremidi::API>; LIBREMIDI_EXPORT libremidi::API midi_api(const input_api_configuration& conf); diff --git a/include/libremidi/libremidi.hpp b/include/libremidi/libremidi.hpp index 9f295894..517fade6 100644 --- a/include/libremidi/libremidi.hpp +++ b/include/libremidi/libremidi.hpp @@ -249,14 +249,18 @@ class LIBREMIDI_EXPORT midi_out // Interop with ni-midi2 #if LIBREMIDI_NI_MIDI2_COMPAT - stdx::error send_ump(const midi::universal_packet& pkt) const { send_ump(pkt.data, pkt.size()); } + stdx::error send_ump(const midi::universal_packet& pkt) const { return send_ump(pkt.data, pkt.size()); } stdx::error send_ump(const midi::sysex7& msg, int group = 0) { - midi::send_sysex7(msg, group, [&](const midi::sysex7_packet& x) { send_ump(x.data); }); + stdx::error ret{}; + midi::send_sysex7(msg, group, [&](const midi::sysex7_packet& x) { ret = send_ump(x.data); }); + return ret; } stdx::error send_ump(const midi::sysex8& msg, int stream, int group = 0) { - midi::send_sysex8(msg, stream, group, [&](const midi::sysex8_packet& x) { send_ump(x.data); }); + stdx::error ret{}; + midi::send_sysex8(msg, stream, group, [&](const midi::sysex8_packet& x) { ret = send_ump(x.data); }); + return ret; } #endif diff --git a/include/libremidi/observer_configuration.hpp b/include/libremidi/observer_configuration.hpp index 514f364d..c2731dfe 100644 --- a/include/libremidi/observer_configuration.hpp +++ b/include/libremidi/observer_configuration.hpp @@ -1,5 +1,6 @@ #pragma once #include +#include NAMESPACE_LIBREMIDI { diff --git a/include/libremidi/ump_endpoint_info.hpp b/include/libremidi/ump_endpoint_info.hpp index 57c695cc..f7ed96fa 100644 --- a/include/libremidi/ump_endpoint_info.hpp +++ b/include/libremidi/ump_endpoint_info.hpp @@ -180,7 +180,7 @@ struct LIBREMIDI_EXPORT ump_endpoint_info // WinMIDI: EndpointDeviceId (std::string), e.g. "\\?\swd#midisrv#midiu_ksa..." // ALSA: sysfs path (std::string), e.g. "/sys/devices/pci0000:00/0000:00:02.2/0000:02:00.0/sound/card0/controlC0" // CoreMIDI: USBVendorProduct (int32_t) - device_identifier device = std::monostate{}; + device_identifier device = libremidi_variant_alias::monostate{}; /// Platform-specific endpoint identifier (for opening) // ALSA Seq: client index