Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Calculate accurate BPM from MIDI clock in ObjC with CoreMIDI

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 !!

like image 589
Yaniv De Ridder Avatar asked Oct 05 '22 20:10

Yaniv De Ridder


1 Answers

There are a few mistakes here, some more important than others.

The most important: You are working with integral numbers of milliseconds, which is not enough precision to get accurate beats/minute. Let's use 120 beats/minute as an example. At 120 beats/minute and 24 clocks/beat, each clock arrives in 20.833 ms. Since you're computing integral milliseconds, that will appear to be either 20 or 21 ms. When you do the math (with a double!) to get back to BPM, that gives you either 125 beats/minute or 119.0476 beats/minute. Neither is what you expect.

If you did the math with integral microseconds or nanoseconds, you would get more accurate values. I suggest using AudioConvertHostTimeToNanos(), defined in <CoreAudio/HostTime.h>, to convert from a MIDITimeStamp to an integral number of nanoseconds, then convert to double and go from there. You shouldn't have to use mach_timebase_info yourself.

Also:

  • MIDIPackets have a timeStamp value that marks when they were received. CoreAudio goes to a lot of trouble to give you that timestamp, so use it!

    Don't rely on a call to mach_absolute_time(), which will be later by an inconsistent amount of time, depending on many factors out of your control.

  • Don't call MIDIPacketListInit.

    To iterate through each MIDIPacket in the MIDIPacketList, use this code, straight from MIDIServices.h:

    MIDIPacket *packet = &packetList->packet[0];
    for (int i = 0; i < packetList->numPackets; ++i) {
        /* your code to use the packet goes here */
        packet = MIDIPacketNext(packet);
    }
    
  • statusByte >> 4 << 4 hurts to look at. You mean statusByte & 0xF0.

like image 79
Kurt Revis Avatar answered Oct 12 '22 19:10

Kurt Revis