2024-10-09 02:01:23 +02:00
|
|
|
#pragma once
|
|
|
|
|
#include <libremidi/detail/memory.hpp>
|
|
|
|
|
#include <libremidi/detail/midi_api.hpp>
|
2024-10-28 19:24:18 +01:00
|
|
|
#include <libremidi/error_handler.hpp>
|
2024-10-09 02:01:23 +02:00
|
|
|
#include <libremidi/input_configuration.hpp>
|
2024-10-28 19:24:18 +01:00
|
|
|
#include <libremidi/backends/coremidi/error_domain.hpp>
|
2024-10-09 02:01:23 +02:00
|
|
|
|
|
|
|
|
#include <CoreMIDI/CoreMIDI.h>
|
|
|
|
|
#include <CoreServices/CoreServices.h>
|
|
|
|
|
|
|
|
|
|
#include <cmath>
|
|
|
|
|
|
|
|
|
|
#include <bit>
|
|
|
|
|
|
|
|
|
|
#if TARGET_OS_IPHONE
|
|
|
|
|
#include <CoreAudio/CoreAudioTypes.h>
|
|
|
|
|
#include <mach/mach_time.h>
|
|
|
|
|
#define LIBREMIDI_AUDIO_GET_CURRENT_HOST_TIME mach_absolute_time
|
|
|
|
|
#else
|
|
|
|
|
#include <CoreAudio/HostTime.h>
|
|
|
|
|
#define LIBREMIDI_AUDIO_GET_CURRENT_HOST_TIME AudioGetCurrentHostTime
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
namespace libremidi
|
|
|
|
|
{
|
|
|
|
|
using CFString_handle = unique_handle<const __CFString, CFRelease>;
|
|
|
|
|
using CFStringMutable_handle = unique_handle<__CFString, CFRelease>;
|
|
|
|
|
namespace
|
|
|
|
|
{
|
|
|
|
|
static inline std::string get_string_property(MIDIObjectRef object, CFStringRef property) noexcept
|
|
|
|
|
{
|
|
|
|
|
CFStringRef res;
|
2024-10-28 19:24:18 +01:00
|
|
|
if (MIDIObjectGetStringProperty(object, property, &res) || !res)
|
|
|
|
|
return {};
|
2024-10-09 02:01:23 +02:00
|
|
|
|
|
|
|
|
char name[256];
|
|
|
|
|
CFStringGetCString(res, name, sizeof(name), kCFStringEncodingUTF8);
|
|
|
|
|
CFRelease(res);
|
|
|
|
|
return name;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static inline int32_t get_int_property(MIDIObjectRef object, CFStringRef property) noexcept
|
|
|
|
|
{
|
|
|
|
|
SInt32 res;
|
|
|
|
|
MIDIObjectGetIntegerProperty(object, property, &res);
|
|
|
|
|
return res;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static inline CFString_handle toCFString(std::string_view str) noexcept
|
|
|
|
|
{
|
|
|
|
|
return CFString_handle{CFStringCreateWithCString(nullptr, str.data(), kCFStringEncodingASCII)};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#if TARGET_OS_IPHONE
|
|
|
|
|
inline uint64_t AudioConvertHostTimeToNanos(uint64_t hostTime)
|
|
|
|
|
{
|
|
|
|
|
static const struct mach_timebase_info timebase = [] {
|
|
|
|
|
struct mach_timebase_info theTimeBaseInfo;
|
|
|
|
|
mach_timebase_info(&theTimeBaseInfo);
|
|
|
|
|
return theTimeBaseInfo;
|
|
|
|
|
}();
|
|
|
|
|
const auto numer = timebase.numer;
|
|
|
|
|
const auto denom = timebase.denom;
|
|
|
|
|
|
|
|
|
|
__uint128_t res = hostTime;
|
|
|
|
|
if (numer != denom)
|
|
|
|
|
{
|
|
|
|
|
res *= numer;
|
|
|
|
|
res /= denom;
|
|
|
|
|
}
|
|
|
|
|
return static_cast<uint64_t>(res);
|
|
|
|
|
}
|
|
|
|
|
#endif
|
2025-06-13 19:16:35 +02:00
|
|
|
|
|
|
|
|
static inline auto get_cfstring_property(MIDIObjectRef prop, CFStringRef name)
|
|
|
|
|
{
|
|
|
|
|
CFStringRef str = nullptr;
|
|
|
|
|
MIDIObjectGetStringProperty(prop, name, &str);
|
|
|
|
|
return CFString_handle{str};
|
|
|
|
|
};
|
|
|
|
|
|
2024-10-09 02:01:23 +02:00
|
|
|
// This function was submitted by Douglas Casey Tucker and apparently
|
|
|
|
|
// derived largely from PortMidi.
|
|
|
|
|
inline CFStringRef EndpointName(MIDIEndpointRef endpoint, bool isExternal)
|
|
|
|
|
{
|
|
|
|
|
CFMutableStringRef result = CFStringCreateMutable(nullptr, 0);
|
|
|
|
|
|
|
|
|
|
// Begin with the endpoint's name.
|
2025-06-13 19:16:35 +02:00
|
|
|
if (auto endpoint_name = get_cfstring_property(endpoint, kMIDIPropertyName))
|
2024-10-09 02:01:23 +02:00
|
|
|
{
|
|
|
|
|
CFStringAppend(result, endpoint_name.get());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// some MIDI devices have a leading space in endpoint name. trim
|
|
|
|
|
CFStringTrim(result, CFSTR(" "));
|
|
|
|
|
|
|
|
|
|
MIDIEntityRef entity = 0;
|
|
|
|
|
MIDIDeviceRef device = 0;
|
|
|
|
|
MIDIEndpointGetEntity(endpoint, &entity);
|
|
|
|
|
if (entity == 0)
|
|
|
|
|
goto finish;
|
|
|
|
|
|
|
|
|
|
if (CFStringGetLength(result) == 0)
|
|
|
|
|
{
|
|
|
|
|
// endpoint name has zero length -- try the entity
|
2025-06-13 19:16:35 +02:00
|
|
|
if (auto entity_name = get_cfstring_property(entity, kMIDIPropertyName))
|
2024-10-09 02:01:23 +02:00
|
|
|
{
|
|
|
|
|
CFStringAppend(result, entity_name.get());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// now consider the device's name
|
|
|
|
|
MIDIEntityGetDevice(entity, &device);
|
|
|
|
|
if (device == 0)
|
|
|
|
|
goto finish;
|
|
|
|
|
|
2025-06-13 19:16:35 +02:00
|
|
|
if (auto dev_name = get_cfstring_property(device, kMIDIPropertyName))
|
2024-10-09 02:01:23 +02:00
|
|
|
{
|
|
|
|
|
const auto dev_strlen = CFStringGetLength(dev_name.get());
|
|
|
|
|
|
|
|
|
|
// if an external device has only one entity, throw away
|
|
|
|
|
// the endpoint name and just use the device name
|
|
|
|
|
if (CFStringGetLength(result) == 0
|
|
|
|
|
|| (isExternal && MIDIDeviceGetNumberOfEntities(device) < 2))
|
|
|
|
|
{
|
|
|
|
|
CFStringAppend(result, dev_name.get());
|
|
|
|
|
goto finish;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// does the entity name already start with the device name?
|
|
|
|
|
// (some drivers do this though they shouldn't)
|
|
|
|
|
// if so, do not prepend
|
|
|
|
|
if (CFStringCompareWithOptions(result, dev_name.get(), CFRangeMake(0, dev_strlen), 0)
|
|
|
|
|
!= kCFCompareEqualTo)
|
|
|
|
|
{
|
|
|
|
|
// prepend the device name to the entity name
|
|
|
|
|
if (CFStringGetLength(result) > 0)
|
|
|
|
|
CFStringInsert(result, 0, CFSTR(" "));
|
|
|
|
|
CFStringInsert(result, 0, dev_name.get());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
finish:
|
|
|
|
|
if (CFStringGetLength(result) == 0)
|
|
|
|
|
return CFSTR("No name");
|
|
|
|
|
else
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// This function was submitted by Douglas Casey Tucker and apparently
|
|
|
|
|
// derived largely from PortMidi.
|
|
|
|
|
inline CFStringRef ConnectedEndpointName(MIDIEndpointRef endpoint)
|
|
|
|
|
{
|
|
|
|
|
CFMutableStringRef result = CFStringCreateMutable(nullptr, 0);
|
|
|
|
|
CFStringRef str{};
|
|
|
|
|
|
|
|
|
|
// Does the endpoint have connections?
|
|
|
|
|
CFDataRef connections = nullptr;
|
|
|
|
|
std::size_t nConnected = 0;
|
|
|
|
|
bool anyStrings = false;
|
|
|
|
|
MIDIObjectGetDataProperty(endpoint, kMIDIPropertyConnectionUniqueID, &connections);
|
|
|
|
|
if (connections != nullptr)
|
|
|
|
|
{
|
|
|
|
|
// It has connections, follow them
|
|
|
|
|
// Concatenate the names of all connected devices
|
|
|
|
|
nConnected = CFDataGetLength(connections) / sizeof(MIDIUniqueID);
|
|
|
|
|
if (nConnected)
|
|
|
|
|
{
|
|
|
|
|
const SInt32* pid = (const SInt32*)(CFDataGetBytePtr(connections));
|
|
|
|
|
for (std::size_t i = 0; i < nConnected; ++i, ++pid)
|
|
|
|
|
{
|
|
|
|
|
MIDIUniqueID id = CFSwapInt32BigToHost(*pid);
|
|
|
|
|
MIDIObjectRef connObject;
|
|
|
|
|
MIDIObjectType connObjectType;
|
|
|
|
|
auto err = MIDIObjectFindByUniqueID(id, &connObject, &connObjectType);
|
|
|
|
|
if (err == noErr)
|
|
|
|
|
{
|
|
|
|
|
if (connObjectType == kMIDIObjectType_ExternalSource
|
|
|
|
|
|| connObjectType == kMIDIObjectType_ExternalDestination)
|
|
|
|
|
{
|
|
|
|
|
// Connected to an external device's endpoint (10.3 and later).
|
|
|
|
|
str = EndpointName((MIDIEndpointRef)(connObject), true);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// Connected to an external device (10.2) (or something else,
|
|
|
|
|
// catch-
|
|
|
|
|
str = nullptr;
|
|
|
|
|
MIDIObjectGetStringProperty(connObject, kMIDIPropertyName, &str);
|
|
|
|
|
}
|
|
|
|
|
if (str != nullptr)
|
|
|
|
|
{
|
|
|
|
|
if (anyStrings)
|
|
|
|
|
CFStringAppend(result, CFSTR(", "));
|
|
|
|
|
else
|
|
|
|
|
anyStrings = true;
|
|
|
|
|
CFStringAppend(result, str);
|
|
|
|
|
CFRelease(str);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
CFRelease(connections);
|
|
|
|
|
}
|
|
|
|
|
if (anyStrings)
|
|
|
|
|
return result;
|
|
|
|
|
|
|
|
|
|
CFRelease(result);
|
|
|
|
|
|
|
|
|
|
// Here, either the endpoint had no connections, or we failed to obtain names
|
|
|
|
|
return EndpointName(endpoint, false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
inline MIDIObjectRef
|
|
|
|
|
locate_object(auto& self, const port_information& info, MIDIObjectType requested_type)
|
|
|
|
|
{
|
|
|
|
|
auto uid = std::bit_cast<std::int32_t>((uint32_t)info.port);
|
|
|
|
|
MIDIObjectRef object{};
|
|
|
|
|
MIDIObjectType type{};
|
|
|
|
|
auto ret = MIDIObjectFindByUniqueID(uid, &object, &type);
|
|
|
|
|
if (ret != noErr)
|
|
|
|
|
{
|
2024-10-28 19:24:18 +01:00
|
|
|
self.libremidi_handle_error(
|
|
|
|
|
self.configuration, "cannot find port: " + info.port_name);
|
2024-10-09 02:01:23 +02:00
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (type != requested_type || object == 0)
|
|
|
|
|
{
|
2024-10-28 19:24:18 +01:00
|
|
|
self.libremidi_handle_error(
|
|
|
|
|
self.configuration, "invalid object: " + info.port_name + " : "
|
2024-10-09 02:01:23 +02:00
|
|
|
+ std::to_string(object));
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return object;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A structure to hold variables related to the CoreMIDI API
|
|
|
|
|
// implementation.
|
|
|
|
|
struct coremidi_data
|
|
|
|
|
{
|
|
|
|
|
MIDIClientRef client{};
|
|
|
|
|
MIDIPortRef port{};
|
|
|
|
|
MIDIEndpointRef endpoint = 0;
|
|
|
|
|
|
|
|
|
|
[[nodiscard]] OSStatus init_client(auto& configuration)
|
|
|
|
|
{
|
|
|
|
|
if (configuration.context)
|
|
|
|
|
{
|
|
|
|
|
client = *configuration.context;
|
|
|
|
|
return noErr;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// Set up our client.
|
|
|
|
|
return MIDIClientCreate(
|
|
|
|
|
toCFString(configuration.client_name).get(), nullptr, nullptr, &client);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-28 19:24:18 +01:00
|
|
|
void close_client(auto& self)
|
|
|
|
|
{
|
|
|
|
|
self.client_open_ = std::errc::not_connected;
|
|
|
|
|
if (!self.configuration.context)
|
|
|
|
|
MIDIClientDispose(self.client);
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-09 02:01:23 +02:00
|
|
|
static uint64_t time_in_nanos(MIDITimeStamp tp) noexcept
|
|
|
|
|
{
|
|
|
|
|
if (tp == 0)
|
|
|
|
|
{ // this happens when receiving asynchronous sysex messages
|
|
|
|
|
return clock_gettime_nsec_np(CLOCK_UPTIME_RAW);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
return AudioConvertHostTimeToNanos(tp);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void set_timestamp(auto& self, MIDITimeStamp packet, timestamp& msg) noexcept
|
|
|
|
|
{
|
|
|
|
|
// packet.timeStamp is in mach_absolute_time units
|
|
|
|
|
// We want a timestamp in nanoseconds
|
|
|
|
|
|
|
|
|
|
switch (self.configuration.timestamps)
|
|
|
|
|
{
|
|
|
|
|
case timestamp_mode::NoTimestamp:
|
|
|
|
|
msg = 0;
|
|
|
|
|
return;
|
|
|
|
|
case timestamp_mode::Relative: {
|
|
|
|
|
if (self.firstMessage)
|
|
|
|
|
{
|
|
|
|
|
self.firstMessage = false;
|
|
|
|
|
msg = 0;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
if constexpr (requires { self.continueSysex; })
|
|
|
|
|
{
|
|
|
|
|
if (self.continueSysex)
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto time = time_in_nanos(packet);
|
|
|
|
|
time -= self.last_time;
|
|
|
|
|
msg = time;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case timestamp_mode::Absolute:
|
|
|
|
|
case timestamp_mode::SystemMonotonic:
|
|
|
|
|
if constexpr (requires { self.continueSysex; })
|
|
|
|
|
{
|
|
|
|
|
if (self.continueSysex)
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
msg = time_in_nanos(packet);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case timestamp_mode::Custom: {
|
|
|
|
|
if constexpr (requires { self.continueSysex; })
|
|
|
|
|
{
|
|
|
|
|
if (self.continueSysex)
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
msg = self.configuration.get_timestamp(time_in_nanos(packet));
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-10-28 19:24:18 +01:00
|
|
|
|
|
|
|
|
stdx::error close_port()
|
|
|
|
|
{
|
|
|
|
|
if (this->endpoint)
|
|
|
|
|
{
|
|
|
|
|
MIDIEndpointDispose(this->endpoint);
|
|
|
|
|
this->endpoint = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this->port)
|
|
|
|
|
{
|
|
|
|
|
MIDIPortDispose(this->port);
|
|
|
|
|
this->port = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return stdx::error{};
|
|
|
|
|
}
|
2024-10-09 02:01:23 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
}
|