very, very simple WP with midi CC out
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
#include <libremidi/config.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#if defined(__APPLE__)
|
||||
#if __LP64__
|
||||
typedef unsigned int UInt32;
|
||||
#else
|
||||
typedef unsigned long UInt32;
|
||||
#endif
|
||||
typedef UInt32 MIDIObjectRef;
|
||||
typedef MIDIObjectRef MIDIClientRef;
|
||||
#else
|
||||
using MIDIClientRef = uint32_t;
|
||||
#endif
|
||||
namespace libremidi
|
||||
{
|
||||
|
||||
struct coremidi_input_configuration
|
||||
{
|
||||
std::string client_name = "libremidi client";
|
||||
std::optional<MIDIClientRef> context{};
|
||||
};
|
||||
|
||||
struct coremidi_output_configuration
|
||||
{
|
||||
std::string client_name = "libremidi client";
|
||||
std::optional<MIDIClientRef> context{};
|
||||
};
|
||||
|
||||
struct coremidi_observer_configuration
|
||||
{
|
||||
std::string client_name = "libremidi client";
|
||||
std::function<void(MIDIClientRef)> on_create_context{};
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
#pragma once
|
||||
#include <libremidi/detail/memory.hpp>
|
||||
#include <libremidi/detail/midi_api.hpp>
|
||||
#include <libremidi/input_configuration.hpp>
|
||||
|
||||
#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;
|
||||
MIDIObjectGetStringProperty(object, property, &res);
|
||||
|
||||
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
|
||||
// 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);
|
||||
|
||||
static constexpr auto getProp = [](MIDIObjectRef prop) {
|
||||
CFStringRef str = nullptr;
|
||||
MIDIObjectGetStringProperty(prop, kMIDIPropertyName, &str);
|
||||
return CFString_handle{str};
|
||||
};
|
||||
|
||||
// Begin with the endpoint's name.
|
||||
if (auto endpoint_name = getProp(endpoint))
|
||||
{
|
||||
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
|
||||
if (auto entity_name = getProp(entity))
|
||||
{
|
||||
CFStringAppend(result, entity_name.get());
|
||||
}
|
||||
}
|
||||
|
||||
// now consider the device's name
|
||||
MIDIEntityGetDevice(entity, &device);
|
||||
if (device == 0)
|
||||
goto finish;
|
||||
|
||||
if (auto dev_name = getProp(device))
|
||||
{
|
||||
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)
|
||||
{
|
||||
self.template error<invalid_parameter_error>(
|
||||
self.configuration, "coremidi::locate_object: cannot find port: " + info.port_name);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (type != requested_type || object == 0)
|
||||
{
|
||||
self.template error<invalid_parameter_error>(
|
||||
self.configuration, "coremidi::locate_object: invalid object: " + info.port_name + " : "
|
||||
+ 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);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
#pragma once
|
||||
#include <libremidi/backends/coremidi/config.hpp>
|
||||
#include <libremidi/backends/coremidi/helpers.hpp>
|
||||
#include <libremidi/detail/midi_in.hpp>
|
||||
#include <libremidi/detail/midi_stream_decoder.hpp>
|
||||
|
||||
namespace libremidi
|
||||
{
|
||||
class midi_in_core final
|
||||
: public midi1::in_api
|
||||
, private coremidi_data
|
||||
, public error_handler
|
||||
{
|
||||
public:
|
||||
struct
|
||||
: input_configuration
|
||||
, coremidi_input_configuration
|
||||
{
|
||||
} configuration;
|
||||
|
||||
midi_in_core(input_configuration&& conf, coremidi_input_configuration&& apiconf)
|
||||
: configuration{std::move(conf), std::move(apiconf)}
|
||||
{
|
||||
if (auto result = init_client(configuration); result != noErr)
|
||||
{
|
||||
error<driver_error>(
|
||||
this->configuration,
|
||||
"midi_in_core: error creating MIDI client object: " + std::to_string(result));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
~midi_in_core() override
|
||||
{
|
||||
// Close a connection if it exists.
|
||||
midi_in_core::close_port();
|
||||
|
||||
if (this->endpoint)
|
||||
MIDIEndpointDispose(this->endpoint);
|
||||
|
||||
close_client();
|
||||
}
|
||||
|
||||
void close_client()
|
||||
{
|
||||
if (!configuration.context)
|
||||
MIDIClientDispose(this->client);
|
||||
}
|
||||
|
||||
void set_client_name(std::string_view) override
|
||||
{
|
||||
warning(configuration, "midi_in_core: set_client_name unsupported");
|
||||
}
|
||||
void set_port_name(std::string_view) override
|
||||
{
|
||||
warning(configuration, "midi_in_core: set_port_name unsupported");
|
||||
}
|
||||
|
||||
libremidi::API get_current_api() const noexcept override { return libremidi::API::COREMIDI; }
|
||||
|
||||
bool open_port(const input_port& info, std::string_view portName) override
|
||||
{
|
||||
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, false);
|
||||
|
||||
auto source = locate_object(*this, info, kMIDIObjectType_Source);
|
||||
if (source == 0)
|
||||
return false;
|
||||
|
||||
// Create our local sink
|
||||
MIDIPortRef port;
|
||||
OSStatus result = MIDIInputPortCreate(
|
||||
this->client, toCFString(portName).get(), midiInputCallback, (void*)this, &port);
|
||||
|
||||
if (result != noErr)
|
||||
{
|
||||
close_client();
|
||||
error<driver_error>(
|
||||
this->configuration, "midi_in_core::open_port: error creating macOS MIDI input port: "
|
||||
+ std::to_string(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Make the connection.
|
||||
if (result = MIDIPortConnectSource(port, source, nullptr); result != noErr)
|
||||
{
|
||||
MIDIPortDispose(port);
|
||||
close_client();
|
||||
error<driver_error>(
|
||||
this->configuration, "midi_in_core::open_port: error connecting macOS MIDI input port.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Save our api-specific port information.
|
||||
this->port = port;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool open_virtual_port(std::string_view portName) override
|
||||
{
|
||||
// Create a virtual MIDI input destination.
|
||||
MIDIEndpointRef endpoint;
|
||||
OSStatus result = MIDIDestinationCreate(
|
||||
this->client, toCFString(portName).get(), midiInputCallback, (void*)this, &endpoint);
|
||||
|
||||
if (result != noErr)
|
||||
{
|
||||
error<driver_error>(
|
||||
this->configuration,
|
||||
"midi_in_core::open_virtual_port: error creating virtual macOS MIDI "
|
||||
"destination.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Save our api-specific connection information.
|
||||
this->endpoint = endpoint;
|
||||
return true;
|
||||
}
|
||||
|
||||
void close_port() override
|
||||
{
|
||||
if (this->endpoint)
|
||||
{
|
||||
MIDIEndpointDispose(this->endpoint);
|
||||
this->endpoint = 0;
|
||||
}
|
||||
|
||||
if (this->port)
|
||||
{
|
||||
MIDIPortDispose(this->port);
|
||||
this->port = 0;
|
||||
}
|
||||
}
|
||||
|
||||
timestamp absolute_timestamp() const noexcept override
|
||||
{
|
||||
return coremidi_data::time_in_nanos(LIBREMIDI_AUDIO_GET_CURRENT_HOST_TIME());
|
||||
}
|
||||
|
||||
static void midiInputCallback(const MIDIPacketList* list, void* procRef, void* /*srcRef*/)
|
||||
{
|
||||
static constexpr timestamp_backend_info timestamp_info{
|
||||
.has_absolute_timestamps = true,
|
||||
.absolute_is_monotonic = false,
|
||||
.has_samples = false,
|
||||
};
|
||||
|
||||
auto& self = *(midi_in_core*)procRef;
|
||||
|
||||
const MIDIPacket* packet = &list->packet[0];
|
||||
for (unsigned int i = 0; i < list->numPackets; ++i)
|
||||
{
|
||||
// My interpretation of the CoreMIDI documentation: all message
|
||||
// types, except sysex, are complete within a packet and there may
|
||||
// be several of them in a single packet. Sysex messages can be
|
||||
// broken across multiple packets and PacketLists but are bundled
|
||||
// alone within each packet (these packets do not contain other
|
||||
// message types). If sysex messages are split across multiple
|
||||
// MIDIPacketLists, they must be handled by multiple calls to this
|
||||
// function.
|
||||
|
||||
if (packet->length == 0)
|
||||
{
|
||||
packet = MIDIPacketNext(packet);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto to_ns = [packet] { return time_in_nanos(packet->timeStamp); };
|
||||
self.m_processing.on_bytes_multi(
|
||||
{packet->data, packet->data + packet->length},
|
||||
self.m_processing.timestamp<timestamp_info>(to_ns, 0));
|
||||
|
||||
packet = MIDIPacketNext(packet);
|
||||
}
|
||||
}
|
||||
|
||||
midi1::input_state_machine m_processing{this->configuration};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
#pragma once
|
||||
#include <libremidi/backends/coremidi/config.hpp>
|
||||
#include <libremidi/backends/coremidi/helpers.hpp>
|
||||
#include <libremidi/detail/midi_out.hpp>
|
||||
|
||||
namespace libremidi
|
||||
{
|
||||
class midi_out_core final
|
||||
: public midi1::out_api
|
||||
, private coremidi_data
|
||||
, public error_handler
|
||||
{
|
||||
public:
|
||||
struct
|
||||
: output_configuration
|
||||
, coremidi_output_configuration
|
||||
{
|
||||
} configuration;
|
||||
|
||||
midi_out_core(output_configuration&& conf, coremidi_output_configuration&& apiconf)
|
||||
: configuration{std::move(conf), std::move(apiconf)}
|
||||
{
|
||||
if (auto result = init_client(configuration); result != noErr)
|
||||
{
|
||||
error<driver_error>(
|
||||
this->configuration,
|
||||
"midi_out_core: error creating MIDI client object: " + std::to_string(result));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
~midi_out_core()
|
||||
{
|
||||
midi_out_core::close_port();
|
||||
|
||||
if (this->endpoint)
|
||||
MIDIEndpointDispose(this->endpoint);
|
||||
|
||||
close_client();
|
||||
}
|
||||
|
||||
void close_client()
|
||||
{
|
||||
if (!configuration.context)
|
||||
MIDIClientDispose(this->client);
|
||||
}
|
||||
|
||||
void set_client_name(std::string_view) override
|
||||
{
|
||||
warning(configuration, "midi_out_core: set_client_name unsupported");
|
||||
}
|
||||
void set_port_name(std::string_view) override
|
||||
{
|
||||
warning(configuration, "midi_out_core: set_port_name unsupported");
|
||||
}
|
||||
|
||||
libremidi::API get_current_api() const noexcept override { return libremidi::API::COREMIDI; }
|
||||
|
||||
bool open_port(const output_port& info, std::string_view portName) override
|
||||
{
|
||||
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, false);
|
||||
|
||||
// Find where we want to send
|
||||
auto destination = locate_object(*this, info, kMIDIObjectType_Destination);
|
||||
if (destination == 0)
|
||||
return false;
|
||||
|
||||
// Create our local source
|
||||
MIDIPortRef port;
|
||||
OSStatus result = MIDIOutputPortCreate(this->client, toCFString(portName).get(), &port);
|
||||
if (result != noErr)
|
||||
{
|
||||
close_client();
|
||||
error<driver_error>(
|
||||
this->configuration, "midi_out_core::open_port: error creating macOS MIDI output port.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Save our api-specific connection information.
|
||||
this->port = port;
|
||||
this->destinationId = destination;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool open_virtual_port(std::string_view portName) override
|
||||
{
|
||||
if (this->endpoint)
|
||||
{
|
||||
warning(
|
||||
configuration,
|
||||
"midi_out_core::open_virtual_port: a virtual output port already "
|
||||
"exists!");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create a virtual MIDI output source.
|
||||
MIDIEndpointRef endpoint;
|
||||
OSStatus result = MIDISourceCreate(this->client, toCFString(portName).get(), &endpoint);
|
||||
|
||||
if (result != noErr)
|
||||
{
|
||||
error<driver_error>(
|
||||
this->configuration,
|
||||
"midi_out_core::initialize: error creating macOS virtual MIDI source.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Save our api-specific connection information.
|
||||
this->endpoint = endpoint;
|
||||
return true;
|
||||
}
|
||||
|
||||
void close_port() override
|
||||
{
|
||||
if (this->endpoint)
|
||||
{
|
||||
MIDIEndpointDispose(this->endpoint);
|
||||
this->endpoint = 0;
|
||||
}
|
||||
|
||||
if (this->port)
|
||||
{
|
||||
MIDIPortDispose(this->port);
|
||||
this->port = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void send_message(const unsigned char* message, size_t size) override
|
||||
{
|
||||
unsigned int nBytes = static_cast<unsigned int>(size);
|
||||
if (nBytes == 0)
|
||||
{
|
||||
warning(configuration, "midi_out_core::send_message: no data in message argument!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (message[0] != 0xF0 && nBytes > 3)
|
||||
{
|
||||
warning(
|
||||
configuration,
|
||||
"midi_out_core::send_message: message format problem ... not sysex but "
|
||||
"> 3 bytes?");
|
||||
return;
|
||||
}
|
||||
|
||||
const MIDITimeStamp timestamp = LIBREMIDI_AUDIO_GET_CURRENT_HOST_TIME();
|
||||
|
||||
const ByteCount bufsize = nBytes > 65535 ? 65535 : nBytes;
|
||||
Byte buffer[bufsize + 16]; // pad for other struct members
|
||||
ByteCount listSize = sizeof(buffer);
|
||||
MIDIPacketList* packetList = (MIDIPacketList*)buffer;
|
||||
|
||||
ByteCount remainingBytes = nBytes;
|
||||
while (remainingBytes)
|
||||
{
|
||||
MIDIPacket* packet = MIDIPacketListInit(packetList);
|
||||
// A MIDIPacketList can only contain a maximum of 64K of data, so if our message is longer,
|
||||
// break it up into chunks of 64K or less and send out as a MIDIPacketList with only one
|
||||
// MIDIPacket. Here, we reuse the memory allocated above on the stack for all.
|
||||
ByteCount bytesForPacket = remainingBytes > 65535 ? 65535 : remainingBytes;
|
||||
const Byte* dataStartPtr = (const Byte*)&message[nBytes - remainingBytes];
|
||||
packet = MIDIPacketListAdd(
|
||||
packetList, listSize, packet, timestamp, bytesForPacket, dataStartPtr);
|
||||
remainingBytes -= bytesForPacket;
|
||||
|
||||
if (!packet)
|
||||
{
|
||||
error<driver_error>(
|
||||
this->configuration, "midi_out_core::send_message: could not allocate packet list");
|
||||
return;
|
||||
}
|
||||
|
||||
// Send to any destinations that may have connected to us.
|
||||
if (this->endpoint)
|
||||
{
|
||||
auto result = MIDIReceived(this->endpoint, packetList);
|
||||
if (result != noErr)
|
||||
{
|
||||
warning(
|
||||
this->configuration,
|
||||
"midi_out_core::send_message: error sending MIDI to virtual "
|
||||
"destinations.");
|
||||
}
|
||||
}
|
||||
|
||||
// And send to an explicit destination port if we're connected.
|
||||
if (this->destinationId != 0)
|
||||
{
|
||||
auto result = MIDISend(this->port, this->destinationId, packetList);
|
||||
if (result != noErr)
|
||||
{
|
||||
warning(
|
||||
this->configuration,
|
||||
"midi_out_core::send_message: error sending MIDI message to port.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MIDIEndpointRef destinationId{};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
#pragma once
|
||||
#include <libremidi/backends/coremidi/config.hpp>
|
||||
#include <libremidi/backends/coremidi/helpers.hpp>
|
||||
#include <libremidi/detail/observer.hpp>
|
||||
|
||||
#include <CoreMIDI/CoreMIDI.h>
|
||||
|
||||
#include <bit>
|
||||
|
||||
namespace libremidi
|
||||
{
|
||||
class observer_core
|
||||
: public observer_api
|
||||
, public error_handler
|
||||
{
|
||||
public:
|
||||
struct
|
||||
: observer_configuration
|
||||
, coremidi_observer_configuration
|
||||
{
|
||||
} configuration;
|
||||
|
||||
explicit observer_core(observer_configuration&& conf, coremidi_observer_configuration&& apiconf)
|
||||
: configuration{std::move(conf), std::move(apiconf)}
|
||||
{
|
||||
if (configuration.client_name.empty())
|
||||
configuration.client_name = "libremidi observer";
|
||||
|
||||
if (!configuration.has_callbacks())
|
||||
return;
|
||||
|
||||
auto result = MIDIClientCreate(
|
||||
toCFString(configuration.client_name).get(),
|
||||
+[](const MIDINotification* message, void* ctx) {
|
||||
((observer_core*)ctx)->notify(message);
|
||||
},
|
||||
this, &client);
|
||||
|
||||
if (result != noErr)
|
||||
{
|
||||
error<driver_error>(
|
||||
this->configuration,
|
||||
"midi_in_core: error creating MIDI client object: " + std::to_string(result));
|
||||
return;
|
||||
}
|
||||
|
||||
if (configuration.on_create_context)
|
||||
configuration.on_create_context(client);
|
||||
|
||||
if (configuration.notify_in_constructor)
|
||||
{
|
||||
if (this->configuration.input_added)
|
||||
for (auto& p : get_input_ports())
|
||||
this->configuration.input_added(p);
|
||||
|
||||
if (this->configuration.output_added)
|
||||
for (auto& p : get_output_ports())
|
||||
this->configuration.output_added(p);
|
||||
}
|
||||
}
|
||||
|
||||
libremidi::API get_current_api() const noexcept override { return libremidi::API::COREMIDI; }
|
||||
|
||||
template <bool Input>
|
||||
auto to_port_info(MIDIObjectRef obj) const noexcept
|
||||
-> std::optional<std::conditional_t<Input, input_port, output_port>>
|
||||
{
|
||||
MIDIEntityRef e{};
|
||||
MIDIEndpointGetEntity(obj, &e);
|
||||
bool physical = bool(e);
|
||||
|
||||
bool ok = false;
|
||||
if (physical && this->configuration.track_hardware)
|
||||
ok = true;
|
||||
else if ((!physical) && this->configuration.track_virtual)
|
||||
ok = true;
|
||||
|
||||
if (!ok)
|
||||
return {};
|
||||
|
||||
return std::conditional_t<Input, input_port, output_port>{
|
||||
{.client = (std::uintptr_t)this->client,
|
||||
.port = std::bit_cast<uint32_t>(get_int_property(obj, kMIDIPropertyUniqueID)),
|
||||
.manufacturer = get_string_property(obj, kMIDIPropertyManufacturer),
|
||||
.device_name = get_string_property(obj, kMIDIPropertyModel),
|
||||
.port_name = get_string_property(obj, kMIDIPropertyName),
|
||||
.display_name = get_string_property(obj, kMIDIPropertyDisplayName)}};
|
||||
}
|
||||
|
||||
std::vector<libremidi::input_port> get_input_ports() const noexcept override
|
||||
{
|
||||
std::vector<libremidi::input_port> ret;
|
||||
|
||||
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, false);
|
||||
for (ItemCount i = 0; i < MIDIGetNumberOfSources(); i++)
|
||||
{
|
||||
if (auto p = to_port_info<true>(MIDIGetSource(i)))
|
||||
ret.push_back(std::move(*p));
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::vector<libremidi::output_port> get_output_ports() const noexcept override
|
||||
{
|
||||
std::vector<libremidi::output_port> ret;
|
||||
|
||||
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, false);
|
||||
for (ItemCount i = 0; i < MIDIGetNumberOfDestinations(); i++)
|
||||
{
|
||||
if (auto p = to_port_info<false>(MIDIGetDestination(i)))
|
||||
ret.push_back(std::move(*p));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
void notify(const MIDINotification* message)
|
||||
{
|
||||
switch (message->messageID)
|
||||
{
|
||||
case kMIDIMsgObjectAdded: {
|
||||
auto obj = reinterpret_cast<const MIDIObjectAddRemoveNotification*>(message);
|
||||
|
||||
switch (obj->childType)
|
||||
{
|
||||
case kMIDIObjectType_Source:
|
||||
if (auto& cb = configuration.input_added)
|
||||
if (auto p = to_port_info<true>(obj->child))
|
||||
cb(std::move(*p));
|
||||
break;
|
||||
case kMIDIObjectType_Destination:
|
||||
if (auto& cb = configuration.output_added)
|
||||
if (auto p = to_port_info<false>(obj->child))
|
||||
cb(std::move(*p));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case kMIDIMsgObjectRemoved: {
|
||||
auto obj = reinterpret_cast<const MIDIObjectAddRemoveNotification*>(message);
|
||||
|
||||
switch (obj->childType)
|
||||
{
|
||||
case kMIDIObjectType_Source:
|
||||
if (auto& cb = configuration.input_removed)
|
||||
if (auto p = to_port_info<true>(obj->child))
|
||||
cb(std::move(*p));
|
||||
break;
|
||||
case kMIDIObjectType_Destination:
|
||||
if (auto& cb = configuration.output_removed)
|
||||
if (auto p = to_port_info<false>(obj->child))
|
||||
cb(std::move(*p));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
~observer_core() { MIDIClientDispose(this->client); }
|
||||
|
||||
private:
|
||||
MIDIClientRef client{};
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user