I'm having some trouble calculating an accurate BPM from a receiving MIDI Clock (using Ableton Live in my tests for sending the MIDI clock).
I'm using CoreMIDI and PGMidi from Pete Goodliffe.
In PGMidi lib there is a method called while MIDI messages are received. From the doc this is happening from a high priority background thread.
Here is my current implementation to calculate the BPM
double BPM;
double currentClockInterval;
uint64_t startClockTime;
- (void) midiSource:(PGMidiSource*)input midiReceived:(const MIDIPacketList *)packetList
{
[self onTick:nil];
MIDIPacket *packet = MIDIPacketListInit((MIDIPacketList*)packetList);
int statusByte = packet->data[0];
int status = statusByte >= 0xf0 ? statusByte : statusByte >> 4 << 4;
switch (status) {
case 0xb0: //cc
//NSLog(@"CC working!");
break;
case 0x90: // Note on, etc...
//NSLog(@"Note on/off working!");
break;
case 0xf8: // Clock tick
if (startClockTime != 0)
{
uint64_t currentClockTime = mach_absolute_time();
currentClockInterval = convertTimeInMilliseconds(currentClockTime - startClockTime);
BPM = (1000 / currentClockInterval / 24) * 60;
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"BPM: %f",BPM);
});
}
startClockTime = mach_absolute_time();
break;
}
}
uint64_t convertTimeInMilliseconds(uint64_t time)
{
const int64_t kOneMillion = 1000 * 1000;
static mach_timebase_info_data_t s_timebase_info;
if (s_timebase_info.denom == 0) {
(void) mach_timebase_info(&s_timebase_info);
}
// mach_absolute_time() returns billionth of seconds,
// so divide by one million to get milliseconds
return (uint64_t)((time * s_timebase_info.numer) / (kOneMillion * s_timebase_info.denom));
}
But for some reasons, the calculated BPM is not accurate. When I send from Ableton Live a BPM below 70 it is fine, but more I send higher BPM less accurate it is for examples:
- Setting 69 BPM in Live give me 69.44444
- 100 -> 104.16666666
- 150 -> 156.250
- 255 -> 277.7777777
Can someone please help me with this? I believe i'm probably not using a good strategy to calculate the BPM. What i'm first calculating the time elapsed between each midi clock using mach_absolute_time().
Thanks for your help!
UPDATE
Following Kurt answer, here is a much more accurate routine that works on iOS (as I'm not using CoreAudio/HostTime.h which is only available on OSX)
double currentClockTime;
double previousClockTime;
- (void) midiSource:(PGMidiSource*)input midiReceived:(const MIDIPacketList *)packetList
{
MIDIPacket *packet = (MIDIPacket*)&packetList->packet[0];
for (int i = 0; i < packetList->numPackets; ++i)
{
int statusByte = packet->data[0];
int status = statusByte >= 0xf0 ? statusByte : statusByte & 0xF0;
if(status == 0xf8)
{
previousClockTime = currentClockTime;
currentClockTime = packet->timeStamp;
if(previousClockTime > 0 && currentClockTime > 0)
{
double intervalInNanoseconds = convertTimeInNanoseconds(currentClockTime-previousClockTime);
BPM = (1000000 / intervalInNanoseconds / 24) * 60;
}
}
packet = MIDIPacketNext(packet);
}
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"BPM: %f",BPM);
});
}
uint64_t convertTimeInNanoseconds(uint64_t time)
{
const int64_t kOneThousand = 1000;
static mach_timebase_info_data_t s_timebase_info;
if (s_timebase_info.denom == 0)
{
(void) mach_timebase_info(&s_timebase_info);
}
// mach_absolute_time() returns billionth of seconds,
// so divide by one thousand to get nanoseconds
return (uint64_t)((time * s_timebase_info.numer) / (kOneThousand * s_timebase_info.denom));
}
As you can see I am now relying on the MidiPacket timeStamp instead of mach_absolute_time() which may be off by an inconstant amount. Also instead of using milliseconds for my BPM calculation I am now using nanoseconds for better accuracy.
With this routine I now get something much more accurate BUT it is still off by a fraction of BPM below 150 and can be off up to 10 BPM on very high BPM (eg. > 400 BPM):
- Setting host to 100 BPM give me 100.401606
- 150 BPM -> 149.700599 ~ 150.602410
- 255 BPM -> 255.102041 ~ 257.731959
- 411 BPM -> 409.836066 ~ 416.666667
Is there something else to consider to get something even more accurate?
Thanks for your help Kurt ! very helpful !
UPDATE 2
I forked PGMidi and added some features such as BPM calculation and Quantization. The repo is here https://github.com/yderidde/PGMidi
I'm sure it can be optimized to be more accurate. Also the quantize routine is not perfect... So if anyone sees some mistake in my code or have suggestions to make the whole thing more stable/accurate , please let me know !!