mirror of
https://github.com/holub/mame
synced 2025-05-31 10:01:51 +03:00
1309 lines
50 KiB
C
1309 lines
50 KiB
C
/*
|
|
* Platform interface to the MacOS X CoreMIDI framework
|
|
*
|
|
* Jon Parise <jparise at cmu.edu>
|
|
* and subsequent work by Andrew Zeldis and Zico Kolter
|
|
* and Roger B. Dannenberg
|
|
*
|
|
* $Id: pmmacosx.c,v 1.17 2002/01/27 02:40:40 jon Exp $
|
|
*/
|
|
|
|
/* Notes:
|
|
|
|
Since the input and output streams are represented by
|
|
MIDIEndpointRef values and almost no other state, we store the
|
|
MIDIEndpointRef on pm_descriptors[midi->device_id].descriptor.
|
|
|
|
OS X does not seem to have an error-code-to-text function, so we
|
|
will just use text messages instead of error codes.
|
|
|
|
Virtual device input synchronization: Once we create a virtual
|
|
device, it is always "on" and receiving messages, but it must drop
|
|
messages unless the device has been opened with Pm_OpenInput. To
|
|
open, the main thread should create all the data structures, then
|
|
call OSMemoryBarrier so that writes are observed, then set
|
|
is_opened = TRUE. To close without locks, we need to get the
|
|
callback to set is_opened to FALSE before we free data structures;
|
|
otherwise, there's a race condition where closing could delete
|
|
structures in use by the virtual_read_callback function. We send
|
|
8 MIDI resets (FF) in a single packet to our own port to signal
|
|
the virtual_read_callback to close it. Then, we wait for the
|
|
callback to recognize the "close" packet and reset is_opened.
|
|
|
|
Device scanning is done when you first open an application.
|
|
PortMIDI does not actively update the devices. Instead, you must
|
|
Pm_Terminate() and Pm_Initialize(), basically starting over. But
|
|
CoreMIDI does not have a way to shut down(!), and even
|
|
MIDIClientDispose() somehow retains state (and docs say do not
|
|
call it even if it worked). The solution, apparently, is to
|
|
call CFRunLoopRunInMode(), which somehow updates CoreMIDI
|
|
state.
|
|
|
|
But when do we call CFRunLoopRunInMode()? I tried calling it
|
|
in midi_in_poll() which is called when you call Pm_Read() since
|
|
that is called often. I observed that this caused the program
|
|
to block for as long as 50ms and fairly often for 2 or 3ms.
|
|
What was Apple thinking? Is it really OK to design systems that
|
|
can only function with a tricky multi-threaded, non-blocking
|
|
priority-based solution, and then not provide a proof of concept
|
|
or documentation? Or is Apple's design really flawed? If anyone
|
|
at Apple reads this, please let me know -- I'm curious.
|
|
|
|
But I digress... Here's the PortMidi approach: Since
|
|
CFRunLoopRunInMode() is potentially a non-realtime operation,
|
|
we only call it in Pm_Initialize(), where other calls to look
|
|
up devices and device names are quite slow to begin with. Again,
|
|
PortMidi does not actively scan for new or deleted devices, so
|
|
if devices change, you won't see it until the next Pm_Terminate
|
|
and Pm_Initialize.
|
|
|
|
Calling CFRunLoopRunInMode() once is probably not enough. There
|
|
might be better way, but it seems to work to just call it 100
|
|
times and insert 20 1ms delays (in case some inter-process
|
|
communication or synchronization is going on).
|
|
This adds 20ms to the wall time of Pm_Initialize(), but it
|
|
typically runs 30ms to much more (~4s), so this has little impact.
|
|
*/
|
|
|
|
#include <stdlib.h>
|
|
|
|
/* turn on lots of debugging print statements */
|
|
#define CM_DEBUG if (0)
|
|
/* #define CM_DEBUG if (1) */
|
|
|
|
#include "portmidi.h"
|
|
#include "pmutil.h"
|
|
#include "pminternal.h"
|
|
#include "porttime.h"
|
|
#include "pmmac.h"
|
|
#include "pmmacosxcm.h"
|
|
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#include <CoreServices/CoreServices.h>
|
|
#include <CoreMIDI/MIDIServices.h>
|
|
#include <CoreAudio/HostTime.h>
|
|
#include <unistd.h>
|
|
#include <libkern/OSAtomic.h>
|
|
|
|
#define PACKET_BUFFER_SIZE 1024
|
|
/* maximum overall data rate (OS X limits MIDI rate in case there
|
|
* is a cycle among IAC ports.
|
|
*/
|
|
|
|
#define MAX_BYTES_PER_S 5400
|
|
|
|
/* Apple reports that packets are dropped when the MIDI bytes/sec
|
|
exceeds 15000. This is computed by "tracking the number of MIDI
|
|
bytes scheduled into 1-second buckets over the last six seconds and
|
|
averaging these counts." This was confirmed in measurements
|
|
(2021) with pm_test/fast.c and pm_test/fastrcv.c Now, in 2022, with
|
|
macOS 12, pm_test/fast{rcv}.c show problems begin at 6000 bytes/sec.
|
|
Previously, we set MAX_BYTES_PER_S to 14000. This is reduced to
|
|
5400 based on testing (which shows 5700 is too high) to fix the
|
|
packet loss problem that showed up with macOS 12.
|
|
|
|
Experiments show this restriction applies to IAC bus MIDI, but not
|
|
to hardware interfaces. (I measured 0.5 Mbps each way over USB to a
|
|
Teensy 3.2 microcontroller implementing a USB MIDI loopback. Maybe
|
|
it would get 1 Mbps one-way, which would make the CoreMIDI
|
|
restriction 18x slower than USB. Maybe other USB MIDI
|
|
implementations are faster -- USB top speed for other protocols is
|
|
certainly higher than 1 Mbps!)
|
|
|
|
This is apparently based on timestamps, not on real time, so we
|
|
have to avoid constructing packets that schedule high speed output
|
|
regardless of when writes occur. The solution is to alter
|
|
timestamps to limit data rates. This adds a slight time
|
|
distortion, e.g. an 11 note chord with all notes on the same
|
|
timestamp will be altered so that the last message is delayed by
|
|
11 messages x 3 bytes/message / 5400 bytes/second = 6.1 ms.
|
|
Note that this is about 2x MIDI speed, but at least 18x slower
|
|
than USB MIDI.
|
|
|
|
Altering timestamps creates another problem, which is that a sender
|
|
that exceeds the maximum rate can queue up an unbounded number of
|
|
messages. With non-USB MIDI devices, you could be writing 5x faster
|
|
to CoreMIDI than the hardware interface can send, causing an
|
|
unbounded backlog, not to mention that the output stream will be a
|
|
steady byte stream (e.g., one 3-byte MIDI message every 0.55 ms),
|
|
losing any original timing or rhythm. PortMidi does not guarantee
|
|
delivery if, over the long run, you write faster than the hardware
|
|
can send.
|
|
|
|
The LIMIT_RATE symbol, if defined (which is the default), enables
|
|
code to modify timestamps for output to an IAC device as follows:
|
|
|
|
Before a packet is formed, the message timestamp is set to the
|
|
maximum of the PortMidi timestamp (converted to CoreMIDI time)
|
|
and min_next_time. After each send, min_next_time is updated to
|
|
the packet time + packet length * delay_per_byte, which limits
|
|
the scheduled bytes-per-second. Also, after each packet list
|
|
flush, min_next_time is updated to the maximum of min_next_time
|
|
and the real time, which prevents many bytes to be scheduled in
|
|
the past. (We could more directly just say packets are never
|
|
scheduled in the past, but we prefer to get the current time -- a
|
|
system call -- only when we perform the more expensive operation
|
|
of flushing packets, so that's when we update min_next_time to
|
|
the current real time. If we are sending a lot, we have to flush
|
|
a lot, so the time will be updated frequently when it matters.)
|
|
|
|
This possible adjustment to timestamps can distort accurate
|
|
timestamps by up to 0.556 us per 3-byte MIDI message.
|
|
|
|
Nothing blocks the sender from queueing up an arbitrary number of
|
|
messages. Timestamps should be used for accurate timing by sending
|
|
timestamped messages a little ahead of real time, not for
|
|
scheduling an entire MIDI sequence at once!
|
|
*/
|
|
#define LIMIT_RATE 1
|
|
|
|
#define SYSEX_BUFFER_SIZE 128
|
|
/* What is the maximum PortMidi device number for an IAC device? A
|
|
* cleaner design would be to not use the endpoint as our device
|
|
* representation. Instead, we could have a private extensible struct
|
|
* to keep all device information, including whether the device is
|
|
* implemented with the AppleMIDIIACDriver, which we need because we
|
|
* have to limit the data rate to this particular driver to avoid
|
|
* dropping messages. Rather than rewrite a lot of code, I am just
|
|
* allocating 64 bytes to flag which devices are IAC ones. If an IAC
|
|
* device number is greater than 63, PortMidi will fail to limit
|
|
* writes to it, but will not complain and will not access memory
|
|
* outside the 64-element array of char.
|
|
*/
|
|
#define MAX_IAC_NUM 63
|
|
|
|
#define VERBOSE_ON 1
|
|
#define VERBOSE if (VERBOSE_ON)
|
|
|
|
#define MIDI_SYSEX 0xf0
|
|
#define MIDI_EOX 0xf7
|
|
#define MIDI_CLOCK 0xf8
|
|
#define MIDI_STATUS_MASK 0x80
|
|
|
|
// "Ref"s are pointers on 32-bit machines and ints on 64 bit machines
|
|
// NULL_REF is our representation of either 0 or NULL
|
|
#ifdef __LP64__
|
|
#define NULL_REF 0
|
|
#else
|
|
#define NULL_REF NULL
|
|
#endif
|
|
|
|
static MIDIClientRef client = NULL_REF; /* Client handle to the MIDI server */
|
|
static MIDIPortRef portIn = NULL_REF; /* Input port handle */
|
|
static MIDIPortRef portOut = NULL_REF; /* Output port handle */
|
|
static char isIAC[MAX_IAC_NUM + 1]; /* is device an IAC device */
|
|
|
|
extern pm_fns_node pm_macosx_in_dictionary;
|
|
extern pm_fns_node pm_macosx_out_dictionary;
|
|
|
|
typedef struct coremidi_info_struct {
|
|
int is_virtual; /* virtual device (TRUE) or actual device (FALSE)? */
|
|
UInt64 delta; /* difference between stream time and real time in ns */
|
|
int sysex_mode; /* middle of sending sysex */
|
|
uint32_t sysex_word; /* accumulate data when receiving sysex */
|
|
uint32_t sysex_byte_count; /* count how many received */
|
|
char error[PM_HOST_ERROR_MSG_LEN];
|
|
char callback_error[PM_HOST_ERROR_MSG_LEN];
|
|
Byte packetBuffer[PACKET_BUFFER_SIZE];
|
|
MIDIPacketList *packetList; /* a pointer to packetBuffer */
|
|
MIDIPacket *packet;
|
|
Byte sysex_buffer[SYSEX_BUFFER_SIZE]; /* temp storage for sysex data */
|
|
MIDITimeStamp sysex_timestamp; /* host timestamp to use with sysex data */
|
|
/* allow for running status (is running status possible here? -rbd): -cpr */
|
|
unsigned char last_command;
|
|
int32_t last_msg_length;
|
|
UInt64 min_next_time; /* when can the next send take place? (host time) */
|
|
int isIACdevice;
|
|
Float64 us_per_host_tick; /* host clock frequency, units of min_next_time */
|
|
UInt64 host_ticks_per_byte; /* host clock units per byte at maximum rate */
|
|
} coremidi_info_node, *coremidi_info_type;
|
|
|
|
/* private function declarations */
|
|
MIDITimeStamp timestamp_pm_to_cm(PmTimestamp timestamp); // returns host time
|
|
PmTimestamp timestamp_cm_to_pm(MIDITimeStamp timestamp); // returns ms
|
|
|
|
char* cm_get_full_endpoint_name(MIDIEndpointRef endpoint, int *isIAC);
|
|
|
|
static PmError check_hosterror(OSStatus err, const char *msg)
|
|
{
|
|
if (err != noErr) {
|
|
sprintf(pm_hosterror_text, "Host error %ld: %s", (long) err, msg);
|
|
pm_hosterror = TRUE;
|
|
return pmHostError;
|
|
}
|
|
return pmNoError;
|
|
}
|
|
|
|
|
|
static int midi_length(int32_t msg)
|
|
{
|
|
int status, high, low;
|
|
static int high_lengths[] = {
|
|
1, 1, 1, 1, 1, 1, 1, 1, /* 0x00 through 0x70 */
|
|
3, 3, 3, 3, 2, 2, 3, 1 /* 0x80 through 0xf0 */
|
|
};
|
|
static int low_lengths[] = {
|
|
1, 2, 3, 2, 1, 1, 1, 1, /* 0xf0 through 0xf8 */
|
|
1, 1, 1, 1, 1, 1, 1, 1 /* 0xf9 through 0xff */
|
|
};
|
|
|
|
status = msg & 0xFF;
|
|
high = status >> 4;
|
|
low = status & 15;
|
|
|
|
return (high != 0xF) ? high_lengths[high] : low_lengths[low];
|
|
}
|
|
|
|
static PmTimestamp midi_synchronize(PmInternal *midi)
|
|
{
|
|
coremidi_info_type info = (coremidi_info_type) midi->api_info;
|
|
UInt64 pm_stream_time_2 = // current time in ns
|
|
AudioConvertHostTimeToNanos(AudioGetCurrentHostTime());
|
|
PmTimestamp real_time; // in ms
|
|
UInt64 pm_stream_time; // in ns
|
|
/* if latency is zero and this is an output, there is no
|
|
time reference and midi_synchronize should never be called */
|
|
assert(midi->time_proc);
|
|
assert(midi->is_input || midi->latency != 0);
|
|
do {
|
|
/* read real_time between two reads of stream time */
|
|
pm_stream_time = pm_stream_time_2;
|
|
real_time = (*midi->time_proc)(midi->time_info);
|
|
pm_stream_time_2 = AudioConvertHostTimeToNanos(
|
|
AudioGetCurrentHostTime());
|
|
/* repeat if more than 0.5 ms has elapsed */
|
|
} while (pm_stream_time_2 > pm_stream_time + 500000);
|
|
info->delta = pm_stream_time - ((UInt64) real_time * (UInt64) 1000000);
|
|
midi->sync_time = real_time;
|
|
return real_time;
|
|
}
|
|
|
|
|
|
static void process_packet(MIDIPacket *packet, PmEvent *event,
|
|
PmInternal *midi, coremidi_info_type info)
|
|
{
|
|
/* handle a packet of MIDI messages from CoreMIDI */
|
|
/* there may be multiple short messages in one packet (!) */
|
|
unsigned int remaining_length = packet->length;
|
|
unsigned char *cur_packet_data = packet->data;
|
|
while (remaining_length > 0) {
|
|
if (cur_packet_data[0] == MIDI_SYSEX ||
|
|
/* are we in the middle of a sysex message? */
|
|
(info->last_command == 0 &&
|
|
!(cur_packet_data[0] & MIDI_STATUS_MASK))) {
|
|
info->last_command = 0; /* no running status */
|
|
unsigned int amt = pm_read_bytes(midi, cur_packet_data,
|
|
remaining_length,
|
|
event->timestamp);
|
|
remaining_length -= amt;
|
|
cur_packet_data += amt;
|
|
} else if (cur_packet_data[0] == MIDI_EOX) {
|
|
/* this should never happen, because pm_read_bytes should
|
|
* get and read all EOX bytes*/
|
|
midi->sysex_in_progress = FALSE;
|
|
info->last_command = 0;
|
|
} else if (cur_packet_data[0] & MIDI_STATUS_MASK) {
|
|
/* compute the length of the next (short) msg in packet */
|
|
unsigned int cur_message_length = midi_length(cur_packet_data[0]);
|
|
if (cur_message_length > remaining_length) {
|
|
#ifdef DEBUG
|
|
printf("PortMidi debug msg: not enough data");
|
|
#endif
|
|
/* since there's no more data, we're done */
|
|
return;
|
|
}
|
|
if (cur_packet_data[0] < MIDI_SYSEX) {
|
|
/* channel messages set running status */
|
|
info->last_command = cur_packet_data[0];
|
|
info->last_msg_length = cur_message_length;
|
|
} else if (cur_packet_data[0] < MIDI_CLOCK) {
|
|
/* system messages clear running status */
|
|
info->last_command = 0;
|
|
info->last_msg_length = 0;
|
|
}
|
|
switch (cur_message_length) {
|
|
case 1:
|
|
event->message = Pm_Message(cur_packet_data[0], 0, 0);
|
|
break;
|
|
case 2:
|
|
event->message = Pm_Message(cur_packet_data[0],
|
|
cur_packet_data[1], 0);
|
|
break;
|
|
case 3:
|
|
event->message = Pm_Message(cur_packet_data[0],
|
|
cur_packet_data[1],
|
|
cur_packet_data[2]);
|
|
break;
|
|
default:
|
|
/* PortMIDI internal error; should never happen */
|
|
assert(cur_message_length == 1);
|
|
return; /* give up on packet if continued after assert */
|
|
}
|
|
pm_read_short(midi, event);
|
|
remaining_length -= cur_message_length;
|
|
cur_packet_data += cur_message_length;
|
|
} else if (info->last_msg_length > remaining_length + 1) {
|
|
/* we have running status, but not enough data */
|
|
#ifdef DEBUG
|
|
printf("PortMidi debug msg: not enough data in CoreMIDI packet");
|
|
#endif
|
|
/* since there's no more data, we're done */
|
|
return;
|
|
} else { /* output message using running status */
|
|
switch (info->last_msg_length) {
|
|
case 1:
|
|
event->message = Pm_Message(info->last_command, 0, 0);
|
|
break;
|
|
case 2:
|
|
event->message = Pm_Message(info->last_command,
|
|
cur_packet_data[0], 0);
|
|
break;
|
|
case 3:
|
|
event->message = Pm_Message(info->last_command,
|
|
cur_packet_data[0],
|
|
cur_packet_data[1]);
|
|
break;
|
|
default:
|
|
/* last_msg_length is invalid -- internal PortMIDI error */
|
|
assert(info->last_msg_length == 1);
|
|
}
|
|
pm_read_short(midi, event);
|
|
remaining_length -= (info->last_msg_length - 1);
|
|
cur_packet_data += (info->last_msg_length - 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/* called when MIDI packets are received */
|
|
static void read_callback(const MIDIPacketList *newPackets, PmInternal *midi)
|
|
{
|
|
PmEvent event;
|
|
MIDIPacket *packet;
|
|
unsigned int packetIndex;
|
|
uint32_t now;
|
|
unsigned int status;
|
|
/* Retrieve the context for this connection */
|
|
coremidi_info_type info = (coremidi_info_type) midi->api_info;
|
|
assert(info);
|
|
|
|
CM_DEBUG printf("read_callback: numPackets %d: ", newPackets->numPackets);
|
|
|
|
/* synchronize time references every 100ms */
|
|
now = (*midi->time_proc)(midi->time_info);
|
|
if (midi->first_message || midi->sync_time + 100 /*ms*/ < now) {
|
|
/* time to resync */
|
|
now = midi_synchronize(midi);
|
|
midi->first_message = FALSE;
|
|
}
|
|
|
|
packet = (MIDIPacket *) &newPackets->packet[0];
|
|
/* hardware devices get untimed messages and apply timestamps. We
|
|
* want to preserve them because they should be more accurate than
|
|
* applying the current time here. virtual devices just pass on the
|
|
* packet->timeStamp, which could be anything. PortMidi says the
|
|
* PortMidi timestamp is the time the message is received. We do not
|
|
* know if we are receiving from a device driver or a virtual device.
|
|
* PortMidi sends to virtual devices get a current timestamp, so we
|
|
* can treat them as the receive time. If the timestamp is zero,
|
|
* suggested by CoreMIDI as the value to use for immediate delivery,
|
|
* then we plug in `now` which is obtained above. If another
|
|
* application sends bogus non-zero timestamps, we will convert them
|
|
* to this port's reference time and pass them as event.timestamp.
|
|
* Receiver beware.
|
|
*/
|
|
CM_DEBUG printf("read_callback packet @ %lld ns (host %lld) "
|
|
"status %x length %d\n",
|
|
AudioConvertHostTimeToNanos(AudioGetCurrentHostTime()),
|
|
AudioGetCurrentHostTime(),
|
|
packet->data[0], packet->length);
|
|
for (packetIndex = 0; packetIndex < newPackets->numPackets; packetIndex++) {
|
|
/* Set the timestamp and dispatch this message */
|
|
CM_DEBUG printf(" packet->timeStamp %lld ns %lld host\n",
|
|
packet->timeStamp,
|
|
AudioConvertHostTimeToNanos(packet->timeStamp));
|
|
if (packet->timeStamp == 0) {
|
|
event.timestamp = now;
|
|
} else {
|
|
event.timestamp = (PmTimestamp) /* explicit conversion */ (
|
|
(AudioConvertHostTimeToNanos(packet->timeStamp) - info->delta) /
|
|
(UInt64) 1000000);
|
|
}
|
|
status = packet->data[0];
|
|
/* process packet as sysex data if it begins with MIDI_SYSEX, or
|
|
MIDI_EOX or non-status byte with no running status */
|
|
CM_DEBUG printf(" len %d stat %x\n", packet->length, status);
|
|
if (status == MIDI_SYSEX || status == MIDI_EOX ||
|
|
((!(status & MIDI_STATUS_MASK)) && !info->last_command)) {
|
|
/* previously was: !(status & MIDI_STATUS_MASK)) {
|
|
* but this could mistake running status for sysex data
|
|
*/
|
|
/* reset running status data -cpr */
|
|
info->last_command = 0;
|
|
info->last_msg_length = 0;
|
|
/* printf("sysex packet length: %d\n", packet->length); */
|
|
pm_read_bytes(midi, packet->data, packet->length, event.timestamp);
|
|
} else {
|
|
process_packet(packet, &event, midi, info);
|
|
}
|
|
packet = MIDIPacketNext(packet);
|
|
}
|
|
}
|
|
|
|
/* callback for real devices - redirects to read_callback */
|
|
static void device_read_callback(const MIDIPacketList *newPackets,
|
|
void *refCon, void *connRefCon)
|
|
{
|
|
read_callback(newPackets, (PmInternal *) connRefCon);
|
|
}
|
|
|
|
|
|
/* callback for virtual devices - redirects to read_callback */
|
|
static void virtual_read_callback(const MIDIPacketList *newPackets,
|
|
void *refCon, void *connRefCon)
|
|
{
|
|
/* this refCon is the device ID -- if there is a valid ID and
|
|
the pm_descriptors table has a non-null pointer to a PmInternal,
|
|
then then device is open and should receive this data */
|
|
PmDeviceID id = (PmDeviceID) (size_t) refCon;
|
|
if (id >= 0 && id < pm_descriptor_len) {
|
|
if (pm_descriptors[id].pub.opened) {
|
|
/* check for close request (7 reset status bytes): */
|
|
if (newPackets->numPackets == 1 &&
|
|
newPackets->packet[0].length == 8 &&
|
|
/* CoreMIDI declares packets with 4-byte alignment, so we
|
|
* should be safe to test for 8 0xFF's as 2 32-bit values: */
|
|
*(SInt32 *) &newPackets->packet[0].data[0] == -1 &&
|
|
*(SInt32 *) &newPackets->packet[0].data[4] == -1) {
|
|
CM_DEBUG printf("got close request packet\n");
|
|
pm_descriptors[id].pub.opened = FALSE;
|
|
return;
|
|
} else {
|
|
read_callback(newPackets, pm_descriptors[id].pm_internal);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/* allocate and initialize our internal coremidi connection info */
|
|
static coremidi_info_type create_macosxcm_info(int is_virtual, int is_input)
|
|
{
|
|
coremidi_info_type info = (coremidi_info_type)
|
|
pm_alloc(sizeof(coremidi_info_node));
|
|
if (!info) {
|
|
return NULL;
|
|
}
|
|
info->is_virtual = is_virtual;
|
|
info->delta = 0;
|
|
info->sysex_mode = FALSE;
|
|
info->sysex_word = 0;
|
|
info->sysex_byte_count = 0;
|
|
info->packet = NULL;
|
|
info->last_command = 0;
|
|
info->last_msg_length = 0;
|
|
info->min_next_time = 0;
|
|
info->isIACdevice = FALSE;
|
|
info->us_per_host_tick = 1000000.0 / AudioGetHostClockFrequency();
|
|
info->host_ticks_per_byte =
|
|
(UInt64) (1000000.0 / (info->us_per_host_tick * MAX_BYTES_PER_S));
|
|
info->packetList = (is_input ? NULL :
|
|
(MIDIPacketList *) info->packetBuffer);
|
|
return info;
|
|
}
|
|
|
|
|
|
static PmError midi_in_open(PmInternal *midi, void *driverInfo)
|
|
{
|
|
MIDIEndpointRef endpoint;
|
|
coremidi_info_type info;
|
|
OSStatus macHostError;
|
|
int is_virtual = pm_descriptors[midi->device_id].pub.is_virtual;
|
|
|
|
/* if this is an external device, descriptor is a MIDIEndpointRef.
|
|
* if this is a virtual device for this application, descriptor is NULL.
|
|
*/
|
|
if (!is_virtual) {
|
|
endpoint = (MIDIEndpointRef) (intptr_t)
|
|
pm_descriptors[midi->device_id].descriptor;
|
|
if (endpoint == NULL_REF) {
|
|
return pmInvalidDeviceId;
|
|
}
|
|
}
|
|
|
|
info = create_macosxcm_info(is_virtual, TRUE);
|
|
midi->api_info = info;
|
|
if (!info) {
|
|
return pmInsufficientMemory;
|
|
}
|
|
if (!is_virtual) {
|
|
macHostError = MIDIPortConnectSource(portIn, endpoint, midi);
|
|
if (macHostError != noErr) {
|
|
midi->api_info = NULL;
|
|
pm_free(info);
|
|
return check_hosterror(macHostError,
|
|
"MIDIPortConnectSource() in midi_in_open()");
|
|
}
|
|
}
|
|
return pmNoError;
|
|
}
|
|
|
|
static PmError midi_in_close(PmInternal *midi)
|
|
{
|
|
MIDIEndpointRef endpoint;
|
|
OSStatus macHostError;
|
|
PmError err = pmNoError;
|
|
|
|
coremidi_info_type info = (coremidi_info_type) midi->api_info;
|
|
|
|
if (!info) return pmBadPtr;
|
|
|
|
endpoint = (MIDIEndpointRef) (intptr_t)
|
|
pm_descriptors[midi->device_id].descriptor;
|
|
if (endpoint == NULL_REF) {
|
|
return pmBadPtr;
|
|
}
|
|
|
|
if (!info->is_virtual) {
|
|
/* shut off the incoming messages before freeing data structures */
|
|
macHostError = MIDIPortDisconnectSource(portIn, endpoint);
|
|
/* If the source closes, you get paramErr == -50 here. It seems
|
|
* possible to monitor changes like sources closing by getting
|
|
* notifications ALL changes, but the CoreMIDI documentation is
|
|
* really terrible overall, and it seems easier to just ignore
|
|
* this host error.
|
|
*/
|
|
if (macHostError != noErr && macHostError != -50) {
|
|
pm_hosterror = TRUE;
|
|
err = check_hosterror(macHostError,
|
|
"MIDIPortDisconnectSource() in midi_in_close()");
|
|
}
|
|
} else {
|
|
/* make "close virtual port" message */
|
|
SInt64 close_port_bytes = 0xFFFFFFFFFFFFFFFF;
|
|
/* memory requirements: packet count (4), timestamp (8), length (2),
|
|
* data (8). Total: 22, but we allocate plenty more:
|
|
*/
|
|
Byte packetBuffer[64];
|
|
MIDIPacketList *plist = (MIDIPacketList *) packetBuffer;
|
|
MIDIPacket *packet = MIDIPacketListInit(plist);
|
|
MIDIPacketListAdd(plist, 64, packet, 0, 8,
|
|
(const Byte *) &close_port_bytes);
|
|
macHostError = MIDISend(portOut, endpoint, plist);
|
|
if (macHostError != noErr) {
|
|
err = check_hosterror(macHostError, "MIDISend() (PortMidi close "
|
|
"port packet) in midi_in_close()");
|
|
}
|
|
/* when packet is delivered, callback thread will clear opened;
|
|
* we must wait for that before removing the input queues etc.
|
|
* Maybe this could use signals of some kind, but if signals use
|
|
* locks, locks can cause priority inversion problems, so we will
|
|
* just sleep as needed. On the MIDI timescale, inserting a 0.5ms
|
|
* latency should be OK, as the application has no business
|
|
* opening/closing devices during time-critical moments.
|
|
*
|
|
* We expect the MIDI thread to close the device quickly (<0.5ms),
|
|
* but we wait up to 50ms in case something terrible happens like
|
|
* getting paged out in the middle of deliving packets to this
|
|
* virtual device. If there is still no response, we time out and
|
|
* force the close without the MIDI thread (even this will probably
|
|
* succeed - the problem would be: this thread proceeds to delete
|
|
* the input queues, and the freed memory is reallocated and
|
|
* overwritten so that queues are no longer usable. Meanwhile,
|
|
* the MIDI thread has already begun to deliver packets, so the
|
|
* check for opened == TRUE passed, but MIDI thread does not insert
|
|
* into queue until queue is freed, reallocated and overwritten.
|
|
*/
|
|
for (int i = 0; i < 100; i++) { /* up to 50ms delay */
|
|
if (!pm_descriptors[midi->device_id].pub.opened) {
|
|
break;
|
|
}
|
|
usleep(500); /* 0.5ms */
|
|
}
|
|
pm_descriptors[midi->device_id].pub.opened = FALSE; /* force it */
|
|
}
|
|
midi->api_info = NULL;
|
|
pm_free(info);
|
|
return err;
|
|
}
|
|
|
|
|
|
static PmError midi_out_open(PmInternal *midi, void *driverInfo)
|
|
{
|
|
coremidi_info_type info;
|
|
int is_virtual = pm_descriptors[midi->device_id].pub.is_virtual;
|
|
|
|
info = create_macosxcm_info(is_virtual, FALSE);
|
|
if (midi->device_id <= MAX_IAC_NUM) {
|
|
info->isIACdevice = isIAC[midi->device_id];
|
|
CM_DEBUG printf("midi_out_open isIACdevice %d\n", info->isIACdevice);
|
|
}
|
|
midi->api_info = info;
|
|
if (!info) {
|
|
return pmInsufficientMemory;
|
|
}
|
|
return pmNoError;
|
|
}
|
|
|
|
|
|
static PmError midi_out_close(PmInternal *midi)
|
|
{
|
|
coremidi_info_type info = (coremidi_info_type) midi->api_info;
|
|
if (!info) return pmBadPtr;
|
|
midi->api_info = NULL;
|
|
pm_free(info);
|
|
return pmNoError;
|
|
}
|
|
|
|
|
|
/* MIDIDestinationCreate apparently cannot create a virtual device
|
|
* without a callback and a "refCon" parameter, but when we create
|
|
* a virtual device, we do not want a PortMidi stream yet -- that
|
|
* should wait for the user to open the stream. So, for the refCon,
|
|
* use the PortMidi device ID. The callback will check if the
|
|
* device is opened within PortMidi, and if so, use the pm_descriptors
|
|
* table to locate the corresponding PmStream.
|
|
*/
|
|
static PmError midi_create_virtual(int is_input, const char *name,
|
|
void *device_info)
|
|
{
|
|
OSStatus macHostError;
|
|
MIDIEndpointRef endpoint;
|
|
CFStringRef nameRef;
|
|
PmDeviceID id = pm_add_device("CoreMIDI", name, is_input, TRUE, NULL,
|
|
(is_input ? &pm_macosx_in_dictionary :
|
|
&pm_macosx_out_dictionary));
|
|
if (id < 0) { /* error -- out of memory or name conflict? */
|
|
return id;
|
|
}
|
|
|
|
nameRef = CFStringCreateWithCString(NULL, name, kCFStringEncodingASCII);
|
|
if (is_input) {
|
|
macHostError = MIDIDestinationCreate(client, nameRef,
|
|
virtual_read_callback, (void *) (intptr_t) id,
|
|
&endpoint);
|
|
} else {
|
|
macHostError = MIDISourceCreate(client, nameRef, &endpoint);
|
|
}
|
|
CFRelease(nameRef);
|
|
|
|
if (macHostError != noErr) {
|
|
/* undo the device we just allocated */
|
|
pm_undo_add_device(id);
|
|
return check_hosterror(macHostError, (is_input ?
|
|
"MIDIDestinationCreate() in midi_create_virtual()" :
|
|
"MIDISourceCreate() in midi_create_virtual()"));
|
|
}
|
|
pm_descriptors[id].descriptor = (void *) (intptr_t) endpoint;
|
|
return id;
|
|
}
|
|
|
|
|
|
static PmError midi_delete_virtual(PmDeviceID id)
|
|
{
|
|
MIDIEndpointRef endpoint;
|
|
OSStatus macHostError;
|
|
PmError err = pmNoError;
|
|
|
|
endpoint = (MIDIEndpointRef) (long) pm_descriptors[id].descriptor;
|
|
if (endpoint == NULL_REF) {
|
|
return pmBadPtr;
|
|
}
|
|
macHostError = MIDIEndpointDispose(endpoint);
|
|
return check_hosterror(macHostError,
|
|
"MIDIEndpointDispose() in midi_in_close()");
|
|
}
|
|
|
|
|
|
static PmError midi_abort(PmInternal *midi)
|
|
{
|
|
PmError err = pmNoError;
|
|
OSStatus macHostError;
|
|
MIDIEndpointRef endpoint = (MIDIEndpointRef) (intptr_t)
|
|
pm_descriptors[midi->device_id].descriptor;
|
|
macHostError = MIDIFlushOutput(endpoint);
|
|
return check_hosterror(macHostError,
|
|
"MIDIFlushOutput() in midi_abort()");
|
|
}
|
|
|
|
|
|
static PmError midi_write_flush(PmInternal *midi, PmTimestamp timestamp)
|
|
{
|
|
OSStatus macHostError = noErr;
|
|
coremidi_info_type info = (coremidi_info_type) midi->api_info;
|
|
MIDIEndpointRef endpoint = (MIDIEndpointRef) (intptr_t)
|
|
pm_descriptors[midi->device_id].descriptor;
|
|
assert(info);
|
|
assert(endpoint);
|
|
if (info->packet != NULL) {
|
|
/* out of space, send the buffer and start refilling it */
|
|
/* update min_next_time each flush to support rate limit */
|
|
UInt64 host_now = AudioGetCurrentHostTime();
|
|
if (host_now > info->min_next_time)
|
|
info->min_next_time = host_now;
|
|
if (info->is_virtual) {
|
|
macHostError = MIDIReceived(endpoint, info->packetList);
|
|
} else {
|
|
macHostError = MIDISend(portOut, endpoint, info->packetList);
|
|
}
|
|
info->packet = NULL; /* indicate no data in packetList now */
|
|
}
|
|
return check_hosterror(macHostError, (info->is_virtual ?
|
|
"MIDIReceived() in midi_write()" :
|
|
"MIDISend() in midi_write()"));
|
|
}
|
|
|
|
|
|
static PmError send_packet(PmInternal *midi, Byte *message,
|
|
unsigned int messageLength, MIDITimeStamp timestamp)
|
|
{
|
|
PmError err;
|
|
coremidi_info_type info = (coremidi_info_type) midi->api_info;
|
|
assert(info);
|
|
|
|
CM_DEBUG printf("add %d to packet %p len %d timestamp %lld @ %lld ns "
|
|
"(host %lld)\n",
|
|
message[0], info->packet, messageLength, timestamp,
|
|
AudioConvertHostTimeToNanos(AudioGetCurrentHostTime()),
|
|
AudioGetCurrentHostTime());
|
|
info->packet = MIDIPacketListAdd(info->packetList,
|
|
sizeof(info->packetBuffer), info->packet,
|
|
timestamp, messageLength, message);
|
|
#if defined(LIMIT_SEND_RATE) && (LIMIT_SEND_RATE != 0)
|
|
info->byte_count += messageLength;
|
|
#endif
|
|
if (info->packet == NULL) {
|
|
/* out of space, send the buffer and start refilling it */
|
|
/* make midi->packet non-null to fool midi_write_flush into sending */
|
|
info->packet = (MIDIPacket *) 4;
|
|
/* timestamp is 0 because midi_write_flush ignores timestamp since
|
|
* timestamps are already in packets. The timestamp parameter is here
|
|
* because other API's need it. midi_write_flush can be called
|
|
* from system-independent code that must be cross-API.
|
|
*/
|
|
if ((err = midi_write_flush(midi, 0)) != pmNoError) return err;
|
|
info->packet = MIDIPacketListInit(info->packetList);
|
|
assert(info->packet); /* if this fails, it's a programming error */
|
|
info->packet = MIDIPacketListAdd(info->packetList,
|
|
sizeof(info->packetBuffer), info->packet,
|
|
timestamp, messageLength, message);
|
|
assert(info->packet); /* can't run out of space on first message */
|
|
}
|
|
return pmNoError;
|
|
}
|
|
|
|
|
|
static PmError midi_write_short(PmInternal *midi, PmEvent *event)
|
|
{
|
|
PmTimestamp when = event->timestamp;
|
|
PmMessage what = event->message;
|
|
MIDITimeStamp timestamp;
|
|
coremidi_info_type info = (coremidi_info_type) midi->api_info;
|
|
Byte message[4];
|
|
unsigned int messageLength;
|
|
|
|
if (info->packet == NULL) {
|
|
info->packet = MIDIPacketListInit(info->packetList);
|
|
/* this can never fail, right? failure would indicate something
|
|
unrecoverable */
|
|
assert(info->packet);
|
|
}
|
|
|
|
/* PortMidi specifies that incoming timestamps are the receive
|
|
* time. Devices attach their receive times, but virtual devices
|
|
* do not. Instead, they pass along whatever timestamp was sent to
|
|
* them. We do not know if we are connected to real or virtual
|
|
* device. To avoid wild timestamps on the receiving end, we
|
|
* consider 2 cases: PortMidi timestamp is zero or latency is
|
|
* zero. Both mean send immediately, so we attach the current time
|
|
* which will go out immediately and arrive with a sensible
|
|
* timestamp (not zero and not zero mapped to the client's local
|
|
* time). Otherwise, we assume the timestamp is reasonable. It
|
|
* might be slighly in the past, but we pass it along after
|
|
* translation to MIDITimeStamp units.
|
|
*
|
|
* Compute timestamp: use current time if timestamp is zero or
|
|
* latency is zero. Both mean no timing and send immediately.
|
|
*/
|
|
if (when == 0 || midi->latency == 0) {
|
|
timestamp = AudioGetCurrentHostTime();
|
|
} else { /* translate PortMidi time + latency to CoreMIDI time */
|
|
timestamp = ((UInt64) (when + midi->latency) * (UInt64) 1000000) +
|
|
info->delta;
|
|
timestamp = AudioConvertNanosToHostTime(timestamp);
|
|
}
|
|
|
|
message[0] = Pm_MessageStatus(what);
|
|
message[1] = Pm_MessageData1(what);
|
|
message[2] = Pm_MessageData2(what);
|
|
messageLength = midi_length(what);
|
|
|
|
#ifdef LIMIT_RATE
|
|
/* Make sure we go forward in time. */
|
|
if (timestamp < info->min_next_time) {
|
|
timestamp = info->min_next_time;
|
|
}
|
|
/* Note that if application is way behind and slowly catching up, then
|
|
* timestamps could be increasing faster than real time, and since
|
|
* timestamps are used to estimate data rate, our estimate could be
|
|
* low, causing CoreMIDI to drop packets. This seems very unlikely.
|
|
*/
|
|
if (info->isIACdevice || info->is_virtual) {
|
|
info->min_next_time = timestamp + messageLength *
|
|
info->host_ticks_per_byte;
|
|
}
|
|
#endif
|
|
/* Add this message to the packet list */
|
|
return send_packet(midi, message, messageLength, timestamp);
|
|
}
|
|
|
|
|
|
static PmError midi_begin_sysex(PmInternal *midi, PmTimestamp when)
|
|
{
|
|
UInt64 when_ns;
|
|
coremidi_info_type info = (coremidi_info_type) midi->api_info;
|
|
assert(info);
|
|
info->sysex_byte_count = 0;
|
|
|
|
/* compute timestamp */
|
|
if (when == 0) when = midi->now;
|
|
/* if latency == 0, midi->now is not valid. We will just set it to zero */
|
|
if (midi->latency == 0) when = 0;
|
|
when_ns = ((UInt64) (when + midi->latency) * (UInt64) 1000000) +
|
|
info->delta;
|
|
info->sysex_timestamp =
|
|
(MIDITimeStamp) AudioConvertNanosToHostTime(when_ns);
|
|
UInt64 now; /* only make system time call when writing a virtual port */
|
|
if (info->is_virtual && info->sysex_timestamp <
|
|
(now = AudioGetCurrentHostTime())) {
|
|
info->sysex_timestamp = now;
|
|
}
|
|
|
|
if (info->packet == NULL) {
|
|
info->packet = MIDIPacketListInit(info->packetList);
|
|
/* this can never fail, right? failure would indicate something
|
|
unrecoverable */
|
|
assert(info->packet);
|
|
}
|
|
return pmNoError;
|
|
}
|
|
|
|
|
|
static PmError midi_end_sysex(PmInternal *midi, PmTimestamp when)
|
|
{
|
|
PmError err;
|
|
coremidi_info_type info = (coremidi_info_type) midi->api_info;
|
|
assert(info);
|
|
|
|
#ifdef LIMIT_RATE
|
|
/* make sure we go foreward in time */
|
|
if (info->sysex_timestamp < info->min_next_time)
|
|
info->sysex_timestamp = info->min_next_time;
|
|
|
|
if (info->isIACdevice) {
|
|
info->min_next_time = info->sysex_timestamp + info->sysex_byte_count *
|
|
info->host_ticks_per_byte;
|
|
}
|
|
#endif
|
|
|
|
/* now send what's in the buffer */
|
|
err = send_packet(midi, info->sysex_buffer, info->sysex_byte_count,
|
|
info->sysex_timestamp);
|
|
info->sysex_byte_count = 0;
|
|
if (err != pmNoError) {
|
|
info->packet = NULL; /* flush everything in the packet list */
|
|
}
|
|
return err;
|
|
}
|
|
|
|
|
|
static PmError midi_write_byte(PmInternal *midi, unsigned char byte,
|
|
PmTimestamp timestamp)
|
|
{
|
|
coremidi_info_type info = (coremidi_info_type) midi->api_info;
|
|
assert(info);
|
|
if (info->sysex_byte_count >= SYSEX_BUFFER_SIZE) {
|
|
PmError err = midi_end_sysex(midi, timestamp);
|
|
if (err != pmNoError) return err;
|
|
}
|
|
info->sysex_buffer[info->sysex_byte_count++] = byte;
|
|
return pmNoError;
|
|
}
|
|
|
|
|
|
static PmError midi_write_realtime(PmInternal *midi, PmEvent *event)
|
|
{
|
|
/* to send a realtime message during a sysex message, first
|
|
flush all pending sysex bytes into packet list */
|
|
PmError err = midi_end_sysex(midi, 0);
|
|
if (err != pmNoError) return err;
|
|
/* then we can just do a normal midi_write_short */
|
|
return midi_write_short(midi, event);
|
|
}
|
|
|
|
|
|
static unsigned int midi_check_host_error(PmInternal *midi)
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
|
|
MIDITimeStamp timestamp_pm_to_cm(PmTimestamp timestamp)
|
|
{
|
|
UInt64 nanos;
|
|
if (timestamp <= 0) {
|
|
return (MIDITimeStamp)0;
|
|
} else {
|
|
nanos = (UInt64)timestamp * (UInt64)1000000;
|
|
return (MIDITimeStamp)AudioConvertNanosToHostTime(nanos);
|
|
}
|
|
}
|
|
|
|
|
|
PmTimestamp timestamp_cm_to_pm(MIDITimeStamp timestamp)
|
|
{
|
|
UInt64 nanos;
|
|
nanos = AudioConvertHostTimeToNanos(timestamp);
|
|
return (PmTimestamp)(nanos / (UInt64)1000000);
|
|
}
|
|
|
|
|
|
//
|
|
// Code taken from http://developer.apple.com/qa/qa2004/qa1374.html
|
|
//////////////////////////////////////
|
|
// Obtain the name of an endpoint without regard for whether it has connections.
|
|
// The result should be released by the caller.
|
|
CFStringRef EndpointName(MIDIEndpointRef endpoint, bool isExternal, int *isIAC)
|
|
{
|
|
CFMutableStringRef result = CFStringCreateMutable(NULL, 0);
|
|
CFStringRef str;
|
|
*isIAC = FALSE;
|
|
|
|
// begin with the endpoint's name
|
|
str = NULL;
|
|
MIDIObjectGetStringProperty(endpoint, kMIDIPropertyName, &str);
|
|
if (str != NULL) {
|
|
CFStringAppend(result, str);
|
|
CFRelease(str);
|
|
}
|
|
MIDIEntityRef entity = NULL_REF;
|
|
MIDIEndpointGetEntity(endpoint, &entity);
|
|
if (entity == NULL_REF) {
|
|
// probably virtual
|
|
return result;
|
|
}
|
|
if (!isExternal) { /* detect IAC devices */
|
|
//extern const CFStringRef kMIDIPropertyDriverOwner;
|
|
MIDIObjectGetStringProperty(entity, kMIDIPropertyDriverOwner, &str);
|
|
if (str != NULL) {
|
|
char s[32]; /* driver name may truncate, but that's OK */
|
|
CFStringGetCString(str, s, 31, kCFStringEncodingUTF8);
|
|
s[31] = 0; /* make sure it is terminated just to be safe */
|
|
CM_DEBUG printf("driver %s\n", s);
|
|
*isIAC = (strcmp(s, "com.apple.AppleMIDIIACDriver") == 0);
|
|
}
|
|
}
|
|
|
|
if (CFStringGetLength(result) == 0) {
|
|
// endpoint name has zero length -- try the entity
|
|
str = NULL;
|
|
MIDIObjectGetStringProperty(entity, kMIDIPropertyName, &str);
|
|
if (str != NULL) {
|
|
CFStringAppend(result, str);
|
|
CFRelease(str);
|
|
}
|
|
}
|
|
// now consider the device's name
|
|
MIDIDeviceRef device = NULL_REF;
|
|
MIDIEntityGetDevice(entity, &device);
|
|
if (device == NULL_REF)
|
|
return result;
|
|
|
|
str = NULL;
|
|
MIDIObjectGetStringProperty(device, kMIDIPropertyName, &str);
|
|
if (CFStringGetLength(result) == 0) {
|
|
CFRelease(result);
|
|
return str;
|
|
}
|
|
if (str != NULL) {
|
|
// if an external device has only one entity, throw away
|
|
// the endpoint name and just use the device name
|
|
if (isExternal && MIDIDeviceGetNumberOfEntities(device) < 2) {
|
|
CFRelease(result);
|
|
return str;
|
|
} else {
|
|
if (CFStringGetLength(str) == 0) {
|
|
CFRelease(str);
|
|
return result;
|
|
}
|
|
// 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, /* endpoint name */
|
|
str, /* device name */
|
|
CFRangeMake(0, CFStringGetLength(str)), 0) !=
|
|
kCFCompareEqualTo) {
|
|
// prepend the device name to the entity name
|
|
if (CFStringGetLength(result) > 0)
|
|
CFStringInsert(result, 0, CFSTR(" "));
|
|
CFStringInsert(result, 0, str);
|
|
}
|
|
CFRelease(str);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
|
|
// Obtain the name of an endpoint, following connections.
|
|
// The result should be released by the caller.
|
|
static CFStringRef ConnectedEndpointName(MIDIEndpointRef endpoint, int *isIAC)
|
|
{
|
|
CFMutableStringRef result = CFStringCreateMutable(NULL, 0);
|
|
CFStringRef str;
|
|
OSStatus err;
|
|
long i;
|
|
|
|
// Does the endpoint have connections?
|
|
CFDataRef connections = NULL;
|
|
long nConnected = 0;
|
|
bool anyStrings = false;
|
|
err = MIDIObjectGetDataProperty(endpoint, kMIDIPropertyConnectionUniqueID,
|
|
&connections);
|
|
if (connections != NULL) {
|
|
// It has connections, follow them
|
|
// Concatenate the names of all connected devices
|
|
nConnected = CFDataGetLength(connections) /
|
|
(int32_t) sizeof(MIDIUniqueID);
|
|
if (nConnected) {
|
|
const SInt32 *pid = (const SInt32 *)(CFDataGetBytePtr(connections));
|
|
for (i = 0; i < nConnected; ++i, ++pid) {
|
|
MIDIUniqueID id = EndianS32_BtoN(*pid);
|
|
MIDIObjectRef connObject;
|
|
MIDIObjectType connObjectType;
|
|
err = MIDIObjectFindByUniqueID(id, &connObject,
|
|
&connObjectType);
|
|
if (err == noErr) {
|
|
if (connObjectType == kMIDIObjectType_ExternalSource ||
|
|
connObjectType == kMIDIObjectType_ExternalDestination) {
|
|
// Connected to an external device's endpoint (>=10.3)
|
|
str = EndpointName((MIDIEndpointRef)(connObject), true,
|
|
isIAC);
|
|
} else {
|
|
// Connected to an external device (10.2)
|
|
// (or something else, catch-all)
|
|
str = NULL;
|
|
MIDIObjectGetStringProperty(connObject,
|
|
kMIDIPropertyName, &str);
|
|
}
|
|
if (str != NULL) {
|
|
if (anyStrings)
|
|
CFStringAppend(result, CFSTR(", "));
|
|
else anyStrings = true;
|
|
CFStringAppend(result, str);
|
|
CFRelease(str);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
CFRelease(connections);
|
|
}
|
|
if (anyStrings)
|
|
return result; // caller should release result
|
|
|
|
CFRelease(result);
|
|
|
|
// Here, either the endpoint had no connections, or we failed to
|
|
// obtain names for any of them.
|
|
return EndpointName(endpoint, false, isIAC);
|
|
}
|
|
|
|
|
|
char *cm_get_full_endpoint_name(MIDIEndpointRef endpoint, int *isIAC)
|
|
{
|
|
/* Thanks to Dan Wilcox for fixes for Unicode handling */
|
|
CFStringRef fullName = ConnectedEndpointName(endpoint, isIAC);
|
|
CFIndex utf16_len = CFStringGetLength(fullName) + 1;
|
|
CFIndex max_byte_len = CFStringGetMaximumSizeForEncoding(
|
|
utf16_len, kCFStringEncodingUTF8) + 1;
|
|
char* pmname = (char *) pm_alloc(CFStringGetLength(fullName) + 1);
|
|
|
|
/* copy the string into our buffer; note that there may be some wasted
|
|
space, but the total waste is not large */
|
|
CFStringGetCString(fullName, pmname, max_byte_len, kCFStringEncodingUTF8);
|
|
|
|
/* clean up */
|
|
if (fullName) CFRelease(fullName);
|
|
return pmname;
|
|
}
|
|
|
|
|
|
pm_fns_node pm_macosx_in_dictionary = {
|
|
none_write_short,
|
|
none_sysex,
|
|
none_sysex,
|
|
none_write_byte,
|
|
none_write_short,
|
|
none_write_flush,
|
|
none_synchronize,
|
|
midi_in_open,
|
|
midi_abort,
|
|
midi_in_close,
|
|
success_poll,
|
|
midi_check_host_error
|
|
};
|
|
|
|
pm_fns_node pm_macosx_out_dictionary = {
|
|
midi_write_short,
|
|
midi_begin_sysex,
|
|
midi_end_sysex,
|
|
midi_write_byte,
|
|
midi_write_realtime,
|
|
midi_write_flush,
|
|
midi_synchronize,
|
|
midi_out_open,
|
|
midi_abort,
|
|
midi_out_close,
|
|
success_poll,
|
|
midi_check_host_error
|
|
};
|
|
|
|
|
|
/* We do nothing with callbacks, but generating the callbacks also
|
|
* updates CoreMIDI state. Callback may not be essential, but calling
|
|
* the CFRunLoopRunInMode is necessary.
|
|
*/
|
|
void cm_notify(const MIDINotification *msg, void *refCon)
|
|
{
|
|
/* for debugging, trace change notifications:
|
|
const char *descr[] = {
|
|
"undefined (0)",
|
|
"kMIDIMsgSetupChanged",
|
|
"kMIDIMsgObjectAdded",
|
|
"kMIDIMsgObjectRemoved",
|
|
"kMIDIMsgPropertyChanged",
|
|
"kMIDIMsgThruConnectionsChanged",
|
|
"kMIDIMsgSerialPortOwnerChanged",
|
|
"kMIDIMsgIOError"};
|
|
|
|
printf("MIDI Notify, messageID %d (%s)\n", (int) msg->messageID,
|
|
descr[(int) msg->messageID]);
|
|
*/
|
|
return;
|
|
}
|
|
|
|
|
|
PmError pm_macosxcm_init(void)
|
|
{
|
|
ItemCount numInputs, numOutputs, numDevices;
|
|
MIDIEndpointRef endpoint;
|
|
int i;
|
|
OSStatus macHostError = noErr;
|
|
const char *error_text;
|
|
|
|
memset(isIAC, 0, sizeof(isIAC)); /* initialize all FALSE */
|
|
|
|
/* Register interface CoreMIDI with create_virtual fn */
|
|
pm_add_interf("CoreMIDI", &midi_create_virtual, &midi_delete_virtual);
|
|
/* no check for error return because this always succeeds */
|
|
|
|
/* Determine the number of MIDI devices on the system */
|
|
numDevices = MIDIGetNumberOfDevices();
|
|
|
|
/* Return prematurely if no devices exist on the system
|
|
Note that this is not an error. There may be no devices.
|
|
Pm_CountDevices() will return zero, which is correct and
|
|
useful information
|
|
*/
|
|
if (numDevices <= 0) {
|
|
return pmNoError;
|
|
}
|
|
|
|
/* Initialize the client handle */
|
|
if (client == NULL_REF) {
|
|
macHostError = MIDIClientCreate(CFSTR("PortMidi"), &cm_notify, NULL,
|
|
&client);
|
|
} else { /* see notes above on device scanning */
|
|
for (int i = 0; i < 100; i++) {
|
|
// look for any changes before scanning for devices
|
|
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, true);
|
|
if (i % 5 == 0) Pt_Sleep(1); /* insert 20 delays */
|
|
}
|
|
}
|
|
if (macHostError != noErr) {
|
|
error_text = "MIDIClientCreate() in pm_macosxcm_init()";
|
|
goto error_return;
|
|
}
|
|
numInputs = MIDIGetNumberOfSources();
|
|
numOutputs = MIDIGetNumberOfDestinations();
|
|
|
|
/* Create the input port */
|
|
macHostError = MIDIInputPortCreate(client, CFSTR("Input port"),
|
|
device_read_callback, NULL, &portIn);
|
|
if (macHostError != noErr) {
|
|
error_text = "MIDIInputPortCreate() in pm_macosxcm_init()";
|
|
goto error_return;
|
|
}
|
|
|
|
/* Create the output port */
|
|
macHostError = MIDIOutputPortCreate(client, CFSTR("Output port"), &portOut);
|
|
if (macHostError != noErr) {
|
|
error_text = "MIDIOutputPortCreate() in pm_macosxcm_init()";
|
|
goto error_return;
|
|
}
|
|
|
|
/* Iterate over the MIDI input devices */
|
|
for (i = 0; i < numInputs; i++) {
|
|
int isIACflag;
|
|
endpoint = MIDIGetSource(i);
|
|
if (endpoint == NULL_REF) {
|
|
continue;
|
|
}
|
|
/* set the first input we see to the default */
|
|
if (pm_default_input_device_id == -1)
|
|
pm_default_input_device_id = pm_descriptor_len;
|
|
|
|
/* Register this device with PortMidi */
|
|
pm_add_device("CoreMIDI",
|
|
cm_get_full_endpoint_name(endpoint, &isIACflag), TRUE, FALSE,
|
|
(void *) (intptr_t) endpoint, &pm_macosx_in_dictionary);
|
|
}
|
|
|
|
/* Iterate over the MIDI output devices */
|
|
for (i = 0; i < numOutputs; i++) {
|
|
int isIACflag;
|
|
PmDeviceID id;
|
|
endpoint = MIDIGetDestination(i);
|
|
if (endpoint == NULL_REF) {
|
|
continue;
|
|
}
|
|
/* set the first output we see to the default */
|
|
if (pm_default_output_device_id == -1)
|
|
pm_default_output_device_id = pm_descriptor_len;
|
|
|
|
/* Register this device with PortMidi */
|
|
id = pm_add_device("CoreMIDI",
|
|
cm_get_full_endpoint_name(endpoint, &isIACflag), FALSE, FALSE,
|
|
(void *) (intptr_t) endpoint, &pm_macosx_out_dictionary);
|
|
/* if this is an IAC device, tuck that info away for write functions */
|
|
if (isIACflag && id <= MAX_IAC_NUM) {
|
|
isIAC[id] = TRUE;
|
|
}
|
|
}
|
|
return pmNoError;
|
|
|
|
error_return:
|
|
pm_macosxcm_term(); /* clear out any opened ports */
|
|
return check_hosterror(macHostError, error_text);
|
|
}
|
|
|
|
void pm_macosxcm_term(void)
|
|
{
|
|
/* docs say do not explicitly dispose of client
|
|
if (client != NULL_REF) MIDIClientDispose(client); */
|
|
if (portIn != NULL_REF) MIDIPortDispose(portIn);
|
|
if (portOut != NULL_REF) MIDIPortDispose(portOut);
|
|
}
|