5

我在从接收的 MIDI 时钟计算准确的 BPM 时遇到了一些麻烦(在我的测试中使用 Ableton Live 来发送 MIDI 时钟)。

我正在使用 Pete Goodliffe 的 CoreMIDI 和 PGMidi。

在 PGMidi 库中,有一个在接收 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 发送低于 70 的 BPM 时,这很好,但我发送的 BPM 越高,准确度越低,例如:

  • 在 Live 中设置 69 BPM 给我 69.44444
  • 100 -> 104.16666666
  • 150 -> 156.250
  • 255 -> 277.7777777

有人可以帮我吗?我相信我可能没有使用一个好的策略来计算 BPM。我首先使用 mach_absolute_time() 计算每个 midi 时钟之间经过的时间。

谢谢你的帮助!

更新

按照 Kurt 的回答,这是一个更准确的例程,适用于 iOS(因为我没有使用仅在 OSX 上可用的 CoreAudio/HostTime.h)

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 timeStamp 而不是 mach_absolute_time() ,它可能会以不稳定的数量关闭。此外,我现在使用纳秒来获得更好的准确性,而不是使用毫秒来计算 BPM。

有了这个例程,我现在得到了更准确的结果,它仍然低于 150 的 BPM 的一小部分,并且在非常高的 BPM(例如 > 400 BPM)上可以达到 10 BPM:

  • 将主机设置为 100 BPM 给我 100.401606
  • 150 BPM -> 149.700599 ~ 150.602410
  • 255 BPM -> 255.102041 ~ 257.731959
  • 411 BPM -> 409.836066 ~ 416.666667

为了得到更准确的结果,还有什么需要考虑的吗?

感谢您的帮助库尔特!非常有帮助 !

更新 2

我分叉了 PGMidi 并添加了一些功能,例如 BPM 计算和量化。回购在这里https://github.com/yderidde/PGMidi

我相信它可以优化为更准确。量化例程也不完美......所以如果有人在我的代码中看到一些错误或有建议让整个事情更稳定/准确,请告诉我!

4

1 回答 1

4

这里有一些错误,有些比其他错误更重要。

最重要的是:您正在使用整数毫秒,这不足以获得准确的节拍/分钟。让我们以 120 次/分钟为例。在 120 次/分钟和 24 个时钟/次的情况下,每个时钟在 20.833 毫秒内到达。由于您正在计算整数毫秒,因此这似乎是 20 或 21 毫秒。当您进行数学运算(加倍!)以恢复 BPM 时,您可以得到 125 次/分钟或 119.0476 次/分钟。也不是你所期望的。

如果您使用整数微秒或纳秒进行数学运算,您将获得更准确的值。我建议使用AudioConvertHostTimeToNanos(), 中定义的<CoreAudio/HostTime.h>, 将 a 转换MIDITimeStamp为整数纳秒,然后转换为double并从那里开始。你不应该使用mach_timebase_info自己。

还:

  • MIDIPackets 有一个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

于 2012-11-27T06:51:12.167 回答