MacOS X: Virtual MIDI sources
In developing my velocity-aware poi, I needed to read acceleration telemetry from the serial port, and transmit MIDI packets from a virtual source to which other MIDI programs on my Mac could subscribe.
Apple's developer documentation is "sufficient," but there are a number of important concepts I had to understand first before I could begin coding.
CoreMIDI defines MIDI clients, which are actors that transmit and receive MIDI packets. These clients may own MIDIEndpoints: MIDI Sources and MIDI Destinations (abstracted by the generic MIDIEndpointRef).
The CoreMIDI documentation is quite easy to follow if you wish to create a MIDIClient, create a MIDISource, and transmit MIDI packets directly to a destination (identified by name.) However, in my limited MIDI experience, I do know that MIDI sources can generate MIDI packets and transmit them without a particular destination in mind. Other MIDI applications (in my case, Ableton Live) can enumerate the virtual and physical MIDI sources in the computer, and "subscribe" to the output of that MIDI Source.
Roughly, the methodology for accomplishing this (on the Source side (the side I was writing)) is to:
- create a MIDIClientRef
- create a MIDISourceRef owned by that client ref
- generate your MIDI data in a byte buffer (the MIDIPacket struct is not necessary)
- add your MIDI data to a MIDIPacketList
- call MIDIReceived(midi_source, &pktlist)
If that last step has you scratching your head, trust me you're not alone. The order to transmit MIDI from a MIDI Source is neither past-tense, nor related to "receiving." This functionality is documented in the CoreMIDI pdf, but it is far from clear. Perhaps Apple could have named this important method in a way that doesn't suggest its a callback function?
But no matter. I have created a virtual source and I am now successfully transcoding data read from a serial port to a virtual MIDI generator, and controlling Ableton Live using that signal. The output MIDI signal is a "Modulation Control Change" signal [176:1:val]. My efforts were rewarded by the sight of Ableton Live's volume slider under direct control of my wireless accelerometer poi.
Having met success in this endeavor, I'm inclined to screw it to hell and disintermediate the computer entirely. I originally chose to upload my data to the computer using serial because I wanted the flexibility to produce signals other than MIDI from the output of my sensor. However, after struggling with tcsetattr, ioctl, fcntl, and the arcane constants required to properly configure a serial port (especially an uncooperative Keyspan USB-Serial adaptor), I'm going to produce MIDI output directly from the microcontroller. I gain the flexibility of being able to hook it up to any MIDI host. And I realize now that configuring and using MIDI under CoreMIDI is not as difficult as I originally thought. I'll let my MIDI host controller do the work of injecting the data into my computer, and subscribe to that MIDI data like any other program.
And now, for your edification, the code. I've never worked with CoreServices before, and so I'm probably doing all sorts of things wrong here. There's idioms I don't know, and I couldn't find the CoreServices analog of strerror. Comment if you know this stuff better than I do, please!
#include "midi.h" #include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <CoreMIDI/CoreMIDI.h>
MIDIEndpointRef *MIDIInit()
{
MIDIClientRef midi_client;
MIDIPortRef midi_port;
MIDIEndpointRef *midi_source;
midi_source = malloc(sizeof(MIDIEndpointRef));OSStatus err;
CFStringRef client_name_cf = CFStringCreateWithCString(NULL, "poi_client", kCFStringEncodingUTF8);
err = MIDIClientCreate(client_name_cf, NULL, NULL, &midi_client);
if (err != noErr) {
fprintf(stderr, "MIDIClientCreate had problems\n");
exit(1);
}CFStringRef port_name_cf = CFStringCreateWithCString(NULL, "poi_port", kCFStringEncodingUTF8);
err = MIDIOutputPortCreate(midi_client, port_name_cf, &midi_port);
if (err != noErr) {
fprintf(stderr, "MIDIOutputPortCreate had problems\n");
exit(1);
}CFStringRef source_name_cf = CFStringCreateWithCString(NULL, "poi", kCFStringEncodingUTF8);
err = MIDISourceCreate(midi_client, source_name_cf, midi_source);
if (err != noErr) {
fprintf(stderr, "MIDISourceCreate had problems\n");
exit(1);
}return midi_source;
}void MIDISendControl(MIDIEndpointRef *midi_source, unsigned char val)
{
MIDIPacketList pktlist;
MIDIPacket p;
Byte data[3];
p_head = MIDIPacketListInit(&pktlist);
data[0] = 176; // Control change
data[1] = 1; // Modulation
data[2] = val; // Value
MIDIPacketListAdd( &pktlist, sizeof(p), p_head, 0, 3, data);
MIDIReceived(*midi_source, &pktlist);
}