summaryrefslogtreecommitdiff
path: root/portmidi/pm_mac/pmmacosxcm.c
blob: e8b196c4923e781bdb896df337cc0f2058d806c7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
/*
 * 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 "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 */
    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 *iac_flag);

static PmError check_hosterror(OSStatus err, const char *msg)
{
    if (err != noErr) {
        snprintf(pm_hosterror_text, PM_HOST_ERROR_MSG_LEN, "Host error %ld: %s", (long) err, msg);
        pm_hosterror = TRUE;
        return pmHostError;
    }
    return pmNoError;
}


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;
}


/* called when MIDI packets are received */
static void read_callback(const MIDIPacketList *newPackets, PmInternal *midi)
{
    PmTimestamp timestamp;
    MIDIPacket *packet;
    unsigned int packetIndex;
    uint32_t now;
    /* 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) {
            timestamp = now;
        } else {
            timestamp = (PmTimestamp) /* explicit conversion */ (
                (AudioConvertHostTimeToNanos(packet->timeStamp) - info->delta) /
                (UInt64) 1000000);
        }
        pm_read_bytes(midi, packet->data, packet->length, timestamp);
        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->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, kCFStringEncodingUTF8);
    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 ?
                "MIDIDestinationCreateWithProtocol() in midi_create_virtual()" :
                "MIDISourceCreateWithProtocol() in midi_create_virtual()"));
    }

    /* Do we have a manufacturer name? If not, set to "PortMidi" */
    const char *mfr_name = "PortMidi";
    PmSysDepInfo *info = (PmSysDepInfo *) device_info;
    /* the version where pmKeyCoreMidiManufacturer was introduced is 210 */
    if (info && info->structVersion >= 210) {
        int i;
        for (i = 0; i < info->length; i++) {  /* search for key */
            if (info->properties[i].key == pmKeyCoreMidiManufacturer) {
                mfr_name = info->properties[i].value;
                break;
            }  /* no other keys are recognized; they are ignored */
        }
    }
    nameRef = CFStringCreateWithCString(NULL, mfr_name, kCFStringEncodingUTF8);
    MIDIObjectSetStringProperty(endpoint, kMIDIPropertyManufacturer, nameRef);
    CFRelease(nameRef);

    pm_descriptors[id].descriptor = (void *) (intptr_t) endpoint;
    return id;
}


static PmError midi_delete_virtual(PmDeviceID id)
{
    MIDIEndpointRef endpoint;
    OSStatus macHostError;
    
    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)
{
    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 = 0;
    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 LIMIT_SEND_RATE
    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 = pm_midi_length((int32_t) 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 *iac_flag)
{
    CFMutableStringRef result = CFStringCreateMutable(NULL, 0);
    CFStringRef str;
    *iac_flag = 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);
            *iac_flag = (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 *iac_flag)
{
    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,
                                           iac_flag);
                    } 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, iac_flag);
}


char *cm_get_full_endpoint_name(MIDIEndpointRef endpoint, int *iac_flag)
{
    /* Thanks to Dan Wilcox for fixes for Unicode handling */
    CFStringRef fullName = ConnectedEndpointName(endpoint, iac_flag);
    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;
    OSStatus macHostError = noErr;
    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 (int i = 0; i < numInputs; i++) {
        int iac_flag;
        endpoint = MIDIGetSource(i);
        if (endpoint == NULL_REF) {
            continue;
        }
        /* Register this device with PortMidi */
        pm_add_device("CoreMIDI", 
                cm_get_full_endpoint_name(endpoint, &iac_flag), TRUE, FALSE,
                (void *) (intptr_t) endpoint, &pm_macosx_in_dictionary);
    }

    /* Iterate over the MIDI output devices */
    for (int i = 0; i < numOutputs; i++) {
        int iac_flag;
        PmDeviceID id;
        endpoint = MIDIGetDestination(i);
        if (endpoint == NULL_REF) {
            continue;
        }
        /* Register this device with PortMidi */
        id = pm_add_device("CoreMIDI", 
                cm_get_full_endpoint_name(endpoint, &iac_flag), FALSE, FALSE,
                (void *) (intptr_t) endpoint, &pm_macosx_out_dictionary);
        /* if this is an IAC device, tuck that info away for write functions */
        if (iac_flag && 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);
}