Skip to content

Commit 59101ab

Browse files
authored
Refactor: Implement CoreMIDI-side scheduling for macOS MIDI playback (#86)
Refactored music_coremidi_mididevice.mm to leverage CoreMIDI's timestamp-based scheduling for improved timing accuracy. Changes include: - Replaced client-side timing logic with CoreMIDI's host time. - Updated PlayTick to calculate and use future MIDITimeStamp for events. - Refactored PlayerLoop to use a condition variable for synchronization instead of busy-waiting with usleep.
1 parent 387f676 commit 59101ab

File tree

1 file changed

+90
-49
lines changed

1 file changed

+90
-49
lines changed

source/mididevices/music_coremidi_mididevice.mm

Lines changed: 90 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
#include <CoreMIDI/CoreMIDI.h>
3636
#include <CoreFoundation/CoreFoundation.h>
3737
#include <mach/mach_time.h>
38+
#include <CoreAudio/HostTime.h>
3839
#include <pthread.h>
3940
#include <unistd.h> // For usleep
4041

@@ -96,11 +97,8 @@
9697
// Timing
9798
int Tempo;
9899
int Division;
99-
uint64_t StartTime;
100-
101-
double SampleRate = 44100; // Assuming a default sample rate
102-
double SamplesPerTick;
103-
double NextTickIn;
100+
MIDITimeStamp CurrentEventHostTime; // This will track the host time of the current event being processed.
101+
double HostUnitsPerTick; // Conversion factor: Host Time Units per MIDI Tick.
104102
MidiHeader *Events; // Linked list of MIDI headers
105103
uint32_t Position; // Current position in the MidiHeader buffer
106104

@@ -127,9 +125,7 @@
127125
, isOpen(false)
128126
, Tempo(500000) // Default: 120 BPM (500,000 µs per quarter note)
129127
, Division(96) // Default PPQN
130-
, StartTime(0)
131-
, SamplesPerTick(0.0)
132-
, NextTickIn(0.0)
128+
, CurrentEventHostTime(0)
133129
, Events(nullptr)
134130
, Position(0)
135131
{
@@ -304,7 +300,13 @@
304300

305301
void CoreMIDIDevice::CalcTickRate()
306302
{
307-
SamplesPerTick = (double)Tempo * SampleRate / (1000000.0 * Division);
303+
// Tempo is in microseconds per quarter note. Division is PPQN.
304+
// Host time units per second = AudioGetHostClockFrequency()
305+
// Microseconds per second = 1,000,000
306+
// Microseconds per tick = Tempo / Division
307+
// Host time units per tick = (Microseconds per tick) * (Host time units per second / Microseconds per second)
308+
Float64 hostClockFrequency = AudioGetHostClockFrequency();
309+
HostUnitsPerTick = ((double)Tempo / (double)Division) * (hostClockFrequency / 1000000.0);
308310
}
309311

310312
//==========================================================================
@@ -475,8 +477,7 @@
475477

476478
void CoreMIDIDevice::InitPlayback()
477479
{
478-
StartTime = mach_absolute_time();
479-
NextTickIn = 0;
480+
CurrentEventHostTime = AudioGetCurrentHostTime(); // Initialize with current host time
480481
Position = 0;
481482
Events = nullptr;
482483
CalcTickRate();
@@ -494,61 +495,83 @@
494495
{
495496
if (Events == nullptr && Callback)
496497
{
498+
// All events in the current MidiHeader processed, request next buffer
497499
Callback(CallbackData);
498500
}
499501

500502
if (Events == nullptr)
501503
{
504+
// No events available to process.
502505
return 0;
503506
}
504507

505-
uint64_t time = mach_absolute_time();
508+
// Read the delta time (first 4 bytes of the event)
509+
uint32_t *event_ptr = (uint32_t *)(Events->lpData + Position);
510+
uint32_t midi_delta_ticks = event_ptr[0]; // Assuming delta time is the first uint32_t
506511

507-
while (Events != nullptr && NextTickIn <= 0)
508-
{
509-
uint32_t *event = (uint32_t *)(Events->lpData + Position);
510-
uint32_t len;
512+
// Advance CurrentEventHostTime based on delta ticks.
513+
// This timestamp will be used for the current event.
514+
CurrentEventHostTime += (MIDITimeStamp)(midi_delta_ticks * HostUnitsPerTick);
511515

512-
if (event[2] < 0x80000000) // Short message
513-
{
514-
len = 12;
515-
}
516-
else
517-
{
518-
len = 12 + ((MEVENT_EVENTPARM(event[2]) + 3) & ~3);
519-
}
516+
uint32_t len; // Length of the current MIDI event in bytes
517+
uint32_t midi_event_type_param = event_ptr[2]; // This is the actual MIDI event or meta-event info
520518

521-
if (MEVENT_EVENTTYPE(event[2]) == MEVENT_TEMPO)
522-
{
523-
SetTempo(MEVENT_EVENTPARM(event[2]));
524-
}
525-
else if (MEVENT_EVENTTYPE(event[2]) == MEVENT_LONGMSG)
519+
if (midi_event_type_param < 0x80000000) // Short message (midi_event_type_param is the combined status/data bytes)
520+
{
521+
len = 12; // 4 bytes delta time, 4 bytes reserved, 4 bytes MIDI message (up to 3 bytes + padding)
522+
}
523+
else // Long message or meta-event (midi_event_type_param holds type and parameter length)
524+
{
525+
len = 12 + ((MEVENT_EVENTPARM(midi_event_type_param) + 3) & ~3);
526+
}
527+
528+
if (MEVENT_EVENTTYPE(midi_event_type_param) == MEVENT_TEMPO)
529+
{
530+
// Tempo change event, update our internal calculation for future events
531+
SetTempo(MEVENT_EVENTPARM(midi_event_type_param));
532+
}
533+
else if (MEVENT_EVENTTYPE(midi_event_type_param) == MEVENT_LONGMSG)
534+
{
535+
// Long MIDI message (SysEx, etc.), data starts after event_ptr[3]
536+
SendMIDIData((uint8_t *)&event_ptr[3], MEVENT_EVENTPARM(midi_event_type_param), CurrentEventHostTime);
537+
}
538+
else if (MEVENT_EVENTTYPE(midi_event_type_param) == 0) // Short MIDI message (note on/off, control change, etc.)
539+
{
540+
// midi_event_type_param contains the 1, 2, or 3 byte MIDI message
541+
uint8_t msg[3] = { (uint8_t)(midi_event_type_param & 0xff),
542+
(uint8_t)((midi_event_type_param >> 8) & 0xff),
543+
(uint8_t)((midi_event_type_param >> 16) & 0xff) };
544+
int msgLen = 0;
545+
if (msg[0] >= 0xF0) // System messages
526546
{
527-
SendMIDIData((uint8_t *)&event[3], MEVENT_EVENTPARM(event[2]), time);
547+
if (msg[0] == 0xF0 || msg[0] == 0xF7) msgLen = 1; // Start/Stop/Continue/Timing/Active Sensing/Reset (1 byte)
548+
else if (msg[0] == 0xF1 || msg[0] == 0xF3) msgLen = 2; // Time Code Quarter Frame, Song Select (2 bytes)
549+
else if (msg[0] == 0xF2) msgLen = 3; // Song Position Pointer (3 bytes)
550+
else msgLen = 1; // Default to 1 for other unknown system messages
528551
}
529-
else if (MEVENT_EVENTTYPE(event[2]) == 0)
552+
else if (msg[0] >= 0xC0 && msg[0] < 0xE0) // Program Change or Channel Aftertouch (2 bytes)
530553
{
531-
uint8_t msg[3] = { (uint8_t)(event[2] & 0xff), (uint8_t)((event[2] >> 8) & 0xff), (uint8_t)((event[2] >> 16) & 0xff) };
532-
int msgLen = (msg[0] >= 0xC0 && msg[0] < 0xE0) ? 2 : 3;
533-
SendMIDIData(msg, msgLen, time);
554+
msgLen = 2;
534555
}
535-
536-
Position += len;
537-
if (Position >= Events->dwBytesRecorded)
556+
else // Note On/Off, Poly Aftertouch, Control Change, Pitch Bend (3 bytes)
538557
{
539-
Events = Events->lpNext;
540-
Position = 0;
541-
if (Events == nullptr && Callback)
542-
{
543-
Callback(CallbackData);
544-
}
558+
msgLen = 3;
545559
}
560+
SendMIDIData(msg, msgLen, CurrentEventHostTime);
561+
}
562+
// Other MEVENT_EVENTTYPE values (e.g., MEVENT_NOTEON, MEVENT_NOTEOFF etc. from WinMIDI)
563+
// are not directly used here; the raw MIDI message is parsed from event_ptr[2]
546564

547-
if (Events != nullptr)
548-
{
549-
NextTickIn += SamplesPerTick * (*(uint32_t *)(Events->lpData + Position));
550-
}
565+
Position += len;
566+
if (Position >= Events->dwBytesRecorded)
567+
{
568+
// Current MidiHeader buffer exhausted, move to the next one
569+
Events = Events->lpNext;
570+
Position = 0;
551571
}
572+
573+
// Indicate that an event was processed and potentially more are available in the current tick.
574+
// The PlayerLoop will decide when to call PlayTick again.
552575
return 1;
553576
}
554577

@@ -578,8 +601,26 @@
578601
{
579602
while (!ExitRequested)
580603
{
581-
PlayTick();
582-
usleep(10000); // Sleep for 10ms
604+
// Process all available events and schedule them with CoreMIDI
605+
while (Events != nullptr && !Paused && !ExitRequested)
606+
{
607+
// PlayTick returns 1 if an event was processed.
608+
// It will continue to be called until Events becomes nullptr.
609+
PlayTick();
610+
}
611+
612+
// After processing all currently available events, or if paused/exit requested,
613+
// wait for new data, unpause, or exit signal.
614+
std::unique_lock<std::mutex> lock(EventMutex);
615+
EventCV.wait(lock, [&]{
616+
return Paused || ExitRequested || Events != nullptr; // Wake up if paused, exit requested, or new events available
617+
});
618+
619+
// If paused, just wait until unpaused or exit requested
620+
while (Paused && !ExitRequested)
621+
{
622+
EventCV.wait(lock);
623+
}
583624
}
584625
}
585626

0 commit comments

Comments
 (0)