Calculate accurate BPM from MIDI clock in ObjC with CoreMIDI
Asked Answered
J

1

7

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

Jungly answered 26/11, 2012 at 10:17 Comment(0)
T
4

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.

Tragopan answered 27/11, 2012 at 6:51 Comment(5)
Thanks Kurt ! I'm looking into this right now. I'll let you know if it works when implemented. One thing ... <CoreAudio/HostTime.h> doesnt exist in iOS. So I believe i have no other choice than having my own converter to ms or nano sec.Jungly
I updated my question with an implementation following your advices! It is still off but it is already much much more accurate. Maybe there are other optimisation that can be done ?Jungly
Aha, I didn't realize that didn't exist on iOS. You could borrow the working code from this question or ConvertToNanos in CAHostTimeBase in the CoreAudio sample code. I don't even trust myself to get the math right...Tragopan
I don't have any particular suggestions for making it more accurate, without seeing it in action. Are the values consistently off by the same amount, or is there any jitter? MIDI is not the world's most advanced technology and you may not be able to do better; if you want a nice stable round number to show the user, you may have to do some averaging/smoothing over time.Tragopan
It all depends of the host BPM. In all cases, it will never be exactly the same BPM. For example when my host is set to 396.00 BPM I get this console log below: 396.825397 396.825397 396.825397 396.825397 396.825397 396.825397 396.825397 396.825397 396.825397 396.825397 390.625000 396.825397 396.825397 396.825397 396.825397 396.825397 396.825397 390.625000 396.825397 396.825397 396.825397 396.825397 396.825397 396.825397 390.625000 396.825397 396.825397 396.825397 396.825397 396.825397 396.825397Jungly

© 2022 - 2024 — McMap. All rights reserved.