Robin Gareus
e6bed9330f
MIDI playback used samples instead of usec. MIDI capture used time-stamp from previous cycle. buffer-size changes were not applied to MIDI port latency
500 lines
13 KiB
C++
500 lines
13 KiB
C++
/*
|
|
* Copyright (C) 2015-2019 Robin Gareus <robin@gareus.org>
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with this program; if not, write to the Free Software Foundation, Inc.,
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
*/
|
|
|
|
#include <sstream>
|
|
#include <CoreAudio/HostTime.h>
|
|
#undef nil
|
|
|
|
#include "coremidi_io.h"
|
|
#include "coreaudio_backend.h"
|
|
|
|
using namespace ARDOUR;
|
|
|
|
#ifndef NDEBUG
|
|
static int _debug_mode = 0;
|
|
#endif
|
|
|
|
|
|
/**
|
|
* MIDI Data flow
|
|
*
|
|
* (A) INPUT (incoming from outside the application)
|
|
*
|
|
* - midiInputCallback (in its own thread, async WRT process callback):
|
|
* takes OS X MIDIPacket, copies into lock-free ringbuffer
|
|
*
|
|
* - processCallback (in its own thread):
|
|
*
|
|
* (1) loop on all input ports:
|
|
* 1A) call recv_event() to read from ringbuffer into stack buffer, also assign-timestamp,
|
|
* 1B) call parse_events() using stack buffer, when appropriate
|
|
* pushes CoreMidiEvent into std::vector<CoreMidiEvent>
|
|
*
|
|
* (2) in MidiPort::cycle_start() (also part of the process callback call tree), MidiPort::get_midi_buffer()
|
|
* calls CoreAudioBackend::midi_event_get () returns a pointer to the data of the specified CoreMidiEvent
|
|
*/
|
|
|
|
static void notifyProc (const MIDINotification *message, void *refCon) {
|
|
CoreMidiIo *self = static_cast<CoreMidiIo*>(refCon);
|
|
self->notify_proc(message);
|
|
}
|
|
|
|
#ifndef NDEBUG
|
|
static void print_packet (const MIDIPacket *p) {
|
|
fprintf (stderr, "CoreMIDI: Packet %d bytes [ ", p->length);
|
|
for (int bb = 0; bb < p->length; ++bb) {
|
|
fprintf (stderr, "%02x ", ((const uint8_t*)p->data)[bb]);
|
|
}
|
|
fprintf (stderr, "]\n");
|
|
}
|
|
|
|
static void dump_packet_list (const UInt32 numPackets, MIDIPacket const *p) {
|
|
for (UInt32 i = 0; i < numPackets; ++i) {
|
|
print_packet (p);
|
|
p = MIDIPacketNext (p);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
static void midiInputCallback(const MIDIPacketList *list, void *procRef, void *srcRef) {
|
|
CoreMidiIo *self = static_cast<CoreMidiIo*> (procRef);
|
|
if (!self || !self->enabled()) {
|
|
// skip while freewheeling
|
|
#ifndef NDEBUG
|
|
if (_debug_mode & 2) {
|
|
fprintf (stderr, "Ignored Midi Packet while freewheeling:\n");
|
|
dump_packet_list (list->numPackets, &list->packet[0]);
|
|
}
|
|
#endif
|
|
return;
|
|
}
|
|
PBD::RingBuffer<uint8_t> * rb = static_cast<PBD::RingBuffer < uint8_t > *> (srcRef);
|
|
if (!rb) {
|
|
#ifndef NDEBUG
|
|
if (_debug_mode & 4) {
|
|
fprintf (stderr, "Ignored Midi Packet - no ringbuffer:\n");
|
|
dump_packet_list (list->numPackets, &list->packet[0]);
|
|
}
|
|
#endif
|
|
return;
|
|
}
|
|
MIDIPacket const *p = &list->packet[0];
|
|
for (UInt32 i = 0; i < list->numPackets; ++i) {
|
|
uint32_t len = ((p->length + 3)&~3) + sizeof(MIDITimeStamp) + sizeof(UInt16);
|
|
#ifndef NDEBUG
|
|
if (_debug_mode & 1) {
|
|
print_packet (p);
|
|
}
|
|
#endif
|
|
if (rb->write_space() > sizeof(uint32_t) + len) {
|
|
rb->write ((uint8_t*)&len, sizeof(uint32_t));
|
|
rb->write ((uint8_t const*)p, len);
|
|
}
|
|
#ifndef NDEBUG
|
|
else {
|
|
fprintf (stderr, "CoreMIDI: dropped MIDI event\n");
|
|
}
|
|
#endif
|
|
p = MIDIPacketNext (p);
|
|
}
|
|
}
|
|
|
|
static std::string getPropertyString (MIDIObjectRef object, CFStringRef key)
|
|
{
|
|
CFStringRef name = NULL;
|
|
std::string rv = "";
|
|
if (noErr == MIDIObjectGetStringProperty(object, key, &name)) {
|
|
const CFIndex size = CFStringGetMaximumSizeForEncoding(CFStringGetLength(name), kCFStringEncodingUTF8);
|
|
char *tmp = (char*) malloc(size);
|
|
if (CFStringGetCString(name, tmp, size, kCFStringEncodingUTF8)) {
|
|
rv = tmp;
|
|
}
|
|
free(tmp);
|
|
CFRelease(name);
|
|
}
|
|
return rv;
|
|
}
|
|
|
|
static std::string getDisplayName (MIDIObjectRef object) {
|
|
return getPropertyString(object, kMIDIPropertyDisplayName);
|
|
}
|
|
|
|
CoreMidiIo::CoreMidiIo()
|
|
: _midi_client (0)
|
|
, _input_endpoints (0)
|
|
, _output_endpoints (0)
|
|
, _input_ports (0)
|
|
, _output_ports (0)
|
|
, _input_queue (0)
|
|
, _rb (0)
|
|
, _n_midi_in (0)
|
|
, _n_midi_out (0)
|
|
, _send_start (0)
|
|
, _recv_start (0)
|
|
, _recv_end (0)
|
|
, _active (false)
|
|
, _enabled (true)
|
|
, _run (false)
|
|
, _changed_callback (0)
|
|
, _changed_arg (0)
|
|
{
|
|
pthread_mutex_init (&_discovery_lock, 0);
|
|
|
|
#ifndef NDEBUG
|
|
const char *p = getenv ("ARDOUR_COREMIDI_DEBUG");
|
|
if (p && *p) _debug_mode = atoi (p);
|
|
#endif
|
|
}
|
|
|
|
CoreMidiIo::~CoreMidiIo()
|
|
{
|
|
pthread_mutex_lock (&_discovery_lock);
|
|
cleanup();
|
|
if (_midi_client) {
|
|
MIDIClientDispose(_midi_client);
|
|
_midi_client = 0;
|
|
}
|
|
pthread_mutex_unlock (&_discovery_lock);
|
|
pthread_mutex_destroy (&_discovery_lock);
|
|
}
|
|
|
|
void
|
|
CoreMidiIo::cleanup()
|
|
{
|
|
_active = false;
|
|
for (uint32_t i = 0 ; i < _n_midi_in ; ++i) {
|
|
MIDIPortDispose(_input_ports[i]);
|
|
_input_queue[i].clear();
|
|
delete _rb[i];
|
|
}
|
|
for (uint32_t i = 0 ; i < _n_midi_out ; ++i) {
|
|
MIDIPortDispose(_output_ports[i]);
|
|
}
|
|
|
|
free(_input_ports); _input_ports = 0;
|
|
free(_input_endpoints); _input_endpoints = 0;
|
|
free(_input_queue); _input_queue = 0;
|
|
free(_output_ports); _output_ports = 0;
|
|
free(_output_endpoints); _output_endpoints = 0;
|
|
free(_rb); _rb = 0;
|
|
|
|
_n_midi_in = 0;
|
|
_n_midi_out = 0;
|
|
}
|
|
|
|
void
|
|
CoreMidiIo::start_cycle (MIDITimeStamp time_at_cycle_start, double cycle_time_us)
|
|
{
|
|
_send_start = time_at_cycle_start;
|
|
_recv_end = time_at_cycle_start;
|
|
|
|
MIDITimeStamp cycle_time = AudioConvertNanosToHostTime (cycle_time_us);
|
|
if (cycle_time <= time_at_cycle_start) {
|
|
_recv_start = time_at_cycle_start - cycle_time;
|
|
} else {
|
|
_recv_start = 0;
|
|
}
|
|
}
|
|
|
|
void
|
|
CoreMidiIo::notify_proc(const MIDINotification *message)
|
|
{
|
|
switch(message->messageID) {
|
|
case kMIDIMsgSetupChanged:
|
|
/* this one catches all of the added/removed/changed below */
|
|
//printf("kMIDIMsgSetupChanged\n");
|
|
discover();
|
|
break;
|
|
case kMIDIMsgObjectAdded:
|
|
{
|
|
//const MIDIObjectAddRemoveNotification *n = (const MIDIObjectAddRemoveNotification*) message;
|
|
//printf("kMIDIMsgObjectAdded\n");
|
|
}
|
|
break;
|
|
case kMIDIMsgObjectRemoved:
|
|
{
|
|
//const MIDIObjectAddRemoveNotification *n = (const MIDIObjectAddRemoveNotification*) message;
|
|
//printf("kMIDIMsgObjectRemoved\n");
|
|
}
|
|
break;
|
|
case kMIDIMsgPropertyChanged:
|
|
{
|
|
//const MIDIObjectPropertyChangeNotification *n = (const MIDIObjectPropertyChangeNotification*) message;
|
|
//printf("kMIDIMsgObjectRemoved\n");
|
|
}
|
|
break;
|
|
case kMIDIMsgThruConnectionsChanged:
|
|
//printf("kMIDIMsgThruConnectionsChanged\n");
|
|
break;
|
|
case kMIDIMsgSerialPortOwnerChanged:
|
|
//printf("kMIDIMsgSerialPortOwnerChanged\n");
|
|
break;
|
|
case kMIDIMsgIOError:
|
|
fprintf(stderr, "kMIDIMsgIOError\n");
|
|
discover();
|
|
break;
|
|
}
|
|
}
|
|
|
|
size_t
|
|
CoreMidiIo::recv_event (uint32_t port, uint64_t &time, uint8_t *d, size_t &s)
|
|
{
|
|
if (!_active || _recv_start == 0) {
|
|
return 0;
|
|
}
|
|
assert(port < _n_midi_in);
|
|
|
|
const size_t minsize = 1 + sizeof(uint32_t) + sizeof(MIDITimeStamp) + sizeof(UInt16);
|
|
|
|
while (_rb[port]->read_space() > minsize) {
|
|
int32_t packet[1024];
|
|
size_t rv;
|
|
uint32_t s = 0;
|
|
rv = _rb[port]->read((uint8_t*)&s, sizeof(uint32_t));
|
|
assert(rv == sizeof(uint32_t));
|
|
if (s > 1024) {
|
|
_rb[port]->increment_read_idx (s);
|
|
continue;
|
|
}
|
|
rv = _rb[port]->read((uint8_t*)packet, s);
|
|
assert(rv == s);
|
|
_input_queue[port].push_back(std::shared_ptr<CoreMIDIPacket>(new _CoreMIDIPacket ((MIDIPacket*)&packet)));
|
|
}
|
|
|
|
for (CoreMIDIQueue::iterator it = _input_queue[port].begin (); it != _input_queue[port].end (); ) {
|
|
if ((*it)->timeStamp < _recv_end) {
|
|
if ((*it)->timeStamp < _recv_start) {
|
|
uint64_t dt = AudioConvertHostTimeToNanos(_recv_start - (*it)->timeStamp);
|
|
/* note: it used to be 10ms (insert handwavy explanation about percievable latency)
|
|
* turns out some midi-keyboads connected via bluetooth can be late by as much as 50ms
|
|
* https://discourse.ardour.org/t/ardour-not-getting-all-messages-from-midi-keyboard/107618/13?
|
|
*/
|
|
if (dt > 8e7 && (*it)->timeStamp != 0) { // 60ms slack and a timestamp is given
|
|
#ifndef NDEBUG
|
|
printf("Dropped Stale Midi Event. dt:%.2fms\n", dt * 1e-6);
|
|
#endif
|
|
it = _input_queue[port].erase(it);
|
|
continue;
|
|
} else {
|
|
/* events without a valid timestamp, or events that arrived
|
|
* less than 10ms in the past are allowed and
|
|
* queued at the beginning of the cycle:
|
|
* time (relative to cycle start) = 0
|
|
*
|
|
* The latter use needed for the "Avid Artist" Control Surface
|
|
* the OSX driver sends no timestamps.
|
|
*/
|
|
#if 0
|
|
printf("Stale Midi Event. dt:%.2fms\n", dt * 1e-6);
|
|
#endif
|
|
}
|
|
time = 0;
|
|
} else {
|
|
time = AudioConvertHostTimeToNanos((*it)->timeStamp - _recv_start);
|
|
}
|
|
s = std::min(s, (size_t) (*it)->length);
|
|
if (s > 0) {
|
|
memcpy(d, (*it)->data, s);
|
|
}
|
|
_input_queue[port].erase(it);
|
|
return s;
|
|
} else {
|
|
#ifndef NDEBUG
|
|
uint64_t dt = AudioConvertHostTimeToNanos((*it)->timeStamp - _recv_end);
|
|
printf("Postponed future Midi Event. dt:%.2fms\n", dt * 1e-6);
|
|
#endif
|
|
}
|
|
++it;
|
|
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int
|
|
CoreMidiIo::send_event (uint32_t port, double reltime_ns, const uint8_t *d, const size_t s)
|
|
{
|
|
if (!_active || _send_start == 0) {
|
|
return 0;
|
|
}
|
|
|
|
assert(port < _n_midi_out);
|
|
UInt64 ts = AudioConvertHostTimeToNanos(_send_start);
|
|
ts += reltime_ns;
|
|
|
|
// TODO use a single packet list.. queue all events first..
|
|
MIDIPacketList pl;
|
|
|
|
pl.numPackets = 1;
|
|
MIDIPacket *mp = &(pl.packet[0]);
|
|
|
|
mp->timeStamp = AudioConvertNanosToHostTime(ts);
|
|
mp->length = s;
|
|
assert(s < 256);
|
|
memcpy(mp->data, d, s);
|
|
|
|
MIDISend(_output_ports[port], _output_endpoints[port], &pl);
|
|
return 0;
|
|
}
|
|
|
|
|
|
std::string
|
|
CoreMidiIo::port_id (uint32_t port, bool input)
|
|
{
|
|
std::stringstream ss;
|
|
if (input) {
|
|
ss << "system:midi_capture_";
|
|
SInt32 id;
|
|
if (noErr == MIDIObjectGetIntegerProperty(_input_endpoints[port], kMIDIPropertyUniqueID, &id)) {
|
|
ss << (unsigned int)id;
|
|
} else {
|
|
ss << port;
|
|
}
|
|
} else {
|
|
ss << "system:midi_playback_";
|
|
SInt32 id;
|
|
if (noErr == MIDIObjectGetIntegerProperty(_output_endpoints[port], kMIDIPropertyUniqueID, &id)) {
|
|
ss << (unsigned int)id;
|
|
} else {
|
|
ss << port;
|
|
}
|
|
}
|
|
return ss.str();
|
|
}
|
|
|
|
std::string
|
|
CoreMidiIo::port_name (uint32_t port, bool input)
|
|
{
|
|
if (input) {
|
|
if (port < _n_midi_in) {
|
|
return getDisplayName(_input_endpoints[port]);
|
|
}
|
|
} else {
|
|
if (port < _n_midi_out) {
|
|
return getDisplayName(_output_endpoints[port]);
|
|
}
|
|
}
|
|
return "";
|
|
}
|
|
|
|
void
|
|
CoreMidiIo::start () {
|
|
_run = true;
|
|
if (!_midi_client) {
|
|
OSStatus err;
|
|
err = MIDIClientCreate(CFSTR("Ardour"), ¬ifyProc, this, &_midi_client);
|
|
if (noErr != err) {
|
|
fprintf(stderr, "Creating Midi Client failed\n");
|
|
}
|
|
}
|
|
discover();
|
|
}
|
|
|
|
void
|
|
CoreMidiIo::stop ()
|
|
{
|
|
_run = false;
|
|
pthread_mutex_lock (&_discovery_lock);
|
|
cleanup();
|
|
pthread_mutex_unlock (&_discovery_lock);
|
|
#if 0
|
|
if (_midi_client) {
|
|
MIDIClientDispose(_midi_client);
|
|
_midi_client = 0;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void
|
|
CoreMidiIo::discover()
|
|
{
|
|
if (!_run || !_midi_client) { return; }
|
|
|
|
if (pthread_mutex_trylock (&_discovery_lock)) {
|
|
return;
|
|
}
|
|
|
|
cleanup();
|
|
|
|
ItemCount srcCount = MIDIGetNumberOfSources();
|
|
ItemCount dstCount = MIDIGetNumberOfDestinations();
|
|
|
|
if (srcCount > 0) {
|
|
_input_ports = (MIDIPortRef *) malloc (srcCount * sizeof(MIDIPortRef));
|
|
_input_endpoints = (MIDIEndpointRef*) malloc (srcCount * sizeof(MIDIEndpointRef));
|
|
_input_queue = (CoreMIDIQueue*) calloc (srcCount, sizeof(CoreMIDIQueue));
|
|
_rb = (PBD::RingBuffer<uint8_t> **) malloc (srcCount * sizeof(PBD::RingBuffer<uint8_t>*));
|
|
}
|
|
if (dstCount > 0) {
|
|
_output_ports = (MIDIPortRef *) malloc (dstCount * sizeof(MIDIPortRef));
|
|
_output_endpoints = (MIDIEndpointRef*) malloc (dstCount * sizeof(MIDIEndpointRef));
|
|
}
|
|
|
|
for (ItemCount i = 0; i < srcCount; i++) {
|
|
OSStatus err;
|
|
MIDIEndpointRef src = MIDIGetSource(i);
|
|
if (!src) continue;
|
|
#ifndef NDEBUG
|
|
printf("MIDI IN DEVICE: %s\n", getDisplayName(src).c_str());
|
|
#endif
|
|
|
|
CFStringRef port_name;
|
|
port_name = CFStringCreateWithFormat(NULL, NULL, CFSTR("midi_capture_%lu"), i);
|
|
|
|
err = MIDIInputPortCreate (_midi_client, port_name, midiInputCallback, this, &_input_ports[_n_midi_in]);
|
|
if (noErr != err) {
|
|
fprintf(stderr, "Cannot create Midi Output\n");
|
|
continue;
|
|
}
|
|
_rb[_n_midi_in] = new PBD::RingBuffer<uint8_t>(32768);
|
|
_input_queue[_n_midi_in] = CoreMIDIQueue();
|
|
MIDIPortConnectSource(_input_ports[_n_midi_in], src, (void*) _rb[_n_midi_in]);
|
|
CFRelease(port_name);
|
|
_input_endpoints[_n_midi_in] = src;
|
|
++_n_midi_in;
|
|
}
|
|
|
|
for (ItemCount i = 0; i < dstCount; i++) {
|
|
MIDIEndpointRef dst = MIDIGetDestination(i);
|
|
CFStringRef port_name;
|
|
port_name = CFStringCreateWithFormat(NULL, NULL, CFSTR("midi_playback_%lu"), i);
|
|
|
|
OSStatus err;
|
|
err = MIDIOutputPortCreate (_midi_client, port_name, &_output_ports[_n_midi_out]);
|
|
if (noErr != err) {
|
|
fprintf(stderr, "Cannot create Midi Output\n");
|
|
continue;
|
|
}
|
|
|
|
#ifndef NDEBUG
|
|
printf("MIDI OUT DEVICE: %s\n", getDisplayName(dst).c_str());
|
|
#endif
|
|
|
|
MIDIPortConnectSource(_output_ports[_n_midi_out], dst, NULL);
|
|
CFRelease(port_name);
|
|
_output_endpoints[_n_midi_out] = dst;
|
|
++_n_midi_out;
|
|
}
|
|
|
|
if (_changed_callback) {
|
|
_changed_callback(_changed_arg);
|
|
}
|
|
|
|
_active = true;
|
|
pthread_mutex_unlock (&_discovery_lock);
|
|
}
|