Рассчитайте точный BPM из MIDI часов в ObjC с CoreMIDI

У меня возникли проблемы с вычислением точного BPM из принимаемых MIDI Clock (используя Ableton Live в моих тестах для отправки MIDI Clock).

Я использую CoreMIDI и PGMidi от Пита Гудлиффа.

В PGMidi lib есть метод, вызываемый при получении MIDI-сообщений. Из документа это происходит из высокоприоритетного фонового потока.

Вот моя текущая реализация для расчета 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));
}

Но по некоторым причинам рассчитанный BPM не является точным. Когда я посылаю из Ableton Live BPM ниже 70, это нормально, но чем больше я посылаю более высокий BPM, тем менее точным является пример:

  • Установка 69 ударов в минуту в Live дает мне 69,44444
  • 100 -> 104.16666666
  • 150 -> 156,250
  • 255 -> 277,7777777

Может кто-нибудь помочь мне с этим? Я считаю, что я, вероятно, не использую хорошую стратегию для расчета BPM. То, что я сначала вычисляю, сколько времени прошло между каждыми миди-часами, используя mach_absolute_time().

Спасибо за вашу помощь!

ОБНОВИТЬ

После ответа Курта, вот гораздо более точная процедура, которая работает на iOS (поскольку я не использую CoreAudio/HostTime.h, который доступен только в 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));
}

Как вы можете видеть, я теперь полагаюсь на метку времени MidiPacket вместо mach_absolute_time (), которая может быть отключена на непостоянную величину. Кроме того, вместо того, чтобы использовать миллисекунды для расчета BPM, я теперь использую наносекунды для большей точности.

С помощью этой процедуры я теперь получаю нечто гораздо более точное, НО оно все еще отключено на долю BPM ниже 150 и может быть отключено до 10 BPM при очень высоком BPM (например,> 400 BPM):

  • Установка хоста на 100 ударов в минуту дает мне 100.401606
  • 150 ударов в минуту -> 149,700599 ~ 150,602410
  • 255 ударов в минуту -> 255.102041 ~ 257.731959
  • 411 ударов в минуту -> 409,836066 ~ 416,666667

Есть ли что-то еще, чтобы рассмотреть что-то еще более точное?

Спасибо за помощь, Курт! очень полезно!

ОБНОВЛЕНИЕ 2

Я разветвил PGMidi и добавил некоторые функции, такие как вычисление BPM и квантование. Репо здесь https://github.com/yderidde/PGMidi

Я уверен, что это может быть оптимизировано, чтобы быть более точным. Кроме того, процедура квантования не идеальна... Так что, если кто-то увидит какую-то ошибку в моем коде или у вас есть предложения, чтобы сделать все это более стабильным / точным, пожалуйста, дайте мне знать!!

1 ответ

Здесь есть несколько ошибок, некоторые более важные, чем другие.

Самое важное: вы работаете с целыми числами в миллисекундах, что недостаточно для получения точных ударов в минуту. Давайте использовать 120 ударов в минуту в качестве примера. При 120 ударах в минуту и ​​24 ударах в минуту, каждые часы поступают за 20,833 мс. Поскольку вы вычисляете целые миллисекунды, это будет 20 или 21 мс. Когда вы делаете математику (с двойным!), Чтобы вернуться к BPM, это дает вам 125 ударов в минуту или 119,0476 ударов в минуту. Не то, что вы ожидаете.

Если бы вы делали математику с интегральными микросекундами или наносекундами, вы бы получили более точные значения. Я предлагаю использовать AudioConvertHostTimeToNanos(), определенный в <CoreAudio/HostTime.h>, чтобы преобразовать из MIDITimeStamp в целое число наносекунд, а затем преобразовать в double и идти оттуда. Вы не должны использовать mach_timebase_info сам.

Также:

  • MIDIPacketс timeStamp значение, которое отмечает, когда они были получены. CoreAudio приложит много усилий, чтобы дать вам эту временную метку, так что используйте ее!

    Не полагайтесь на звонок mach_absolute_time(), что будет позже через непоследовательное количество времени, в зависимости от многих факторов вне вашего контроля.

  • Не звони MIDIPacketListInit,

    Перебирать каждый MIDIPacket в MIDIPacketList, используйте этот код, прямо из 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 Больно смотреть на. Ты имеешь в виду statusByte & 0xF0,

Другие вопросы по тегам