一个 MIDI 文件的音符有多长(以毫秒为单位)?

How long is a MIDI file note in milliseconds?

所以我有这个非常简单的 MIDI 文件:

我只是想为这 2 个音符定义以毫秒为单位的长度。实时第一个音符不到一秒,第二个音符大约2秒。

当我迭代 MIDI 事件时,每次我得到一个音符打开或音符关闭事件时,我都会在那里获取增量滴答声。我 100% 确定这些增量刻度是正确的,因为我将它们与正常工作的算法进行了比较。

据我了解,如果此文件中的 F5 键在 X 时间内击中音符开,则该事件的音符关将在音符关事件的 deltaTicks 数之后击中。所以这意味着要找到 F5 键的长度(以毫秒为单位),我需要做的就是将音符关闭事件增量滴答声转换为毫秒。 (我说的对吗?)

我解析了 MIDI 文件,所以我有节奏和 PPQ,我搜索了很多相关内容,所以直到现在我找到了这个公式:

double bpm =  (60000000 / mTempo);
deltaTimesInMilliseconds = deltaTicks * (60000 / (bpm * division));
        

但我可能在这里做错了什么,因为 F5 键音符关闭事件的增量时间(以毫秒为单位)约为 7000,这意味着 7 秒。

感谢您的帮助!

文件的更多内容:

    public class MidiParserN {
            private static double mTempo;
            private static short division;
            ...
            ...
            private static TrackChunkEvent 
            parseMTrkEvent() throws Exception {

            int deltaTicks = Math.abs(calculateDeltaTicks(getVariableLengthQuantity()));
            double deltaTimesInMilliseconds = 0;
    
            int eventType = parseEventType();
            int eventNum = -1;
            String eventName = "";

            if (eventType == EventTypes.MIDI_EVENT.ordinal()) {
                eventNum = parseMidiMessage();
                eventName = midiEventsTypes[eventNum].toString();
            } else if (eventType == EventTypes.SYSEX_EVENT.ordinal()) {
                parseSysexEvent();
            } else {
                eventNum = parseAndExecuteMetaEvent();
                eventName = metaEventsTypes[eventNum].toString();
            }
    
            if (eventName.equals("NOTE_ON_EVENT") && deltaTimes != 0 && mTempo != 0) {
                double bpm =  (60000000 / mTempo);
                deltaTimesInMilliseconds = deltaTicks * (60000 / (bpm * division));              
                log("DELTA_TIMES_MILLISECONDS", Double.toString(deltaTimesInMilliseconds));
                    }
       

             else if (eventName.equals("NOTE_OFF_EVENT") && deltaTimes != 0 && mTempo != 0) {
                double bpm =  (60000000 / mTempo);
                deltaTimesInMilliseconds = deltaTicks * (60000 / (bpm * division));
                log("DELTA_TIMES_MILISECONDS", Double.toString(deltaTimesInMilliseconds));
            }
      return new TrackChunkEvent(eventNum, eventName);
            }
...
...
}

正在更新速度事件的速度:

  if (firstByteOfMetaEvent == 0x51 && secondByteOfMetaEvent == 3) {
            int tempo = byteArrayToInt(parseMetaEventSetTempo());
            mTempo = tempo;
            return MetaEventsTypes.SET_TEMPO.ordinal();
        }

MIDI 文件:

4d 54 68 64 00 00 00 06 00 01 00 02 01 80 4d 54 72 6b 00 00 00 13 00 ff 58 04 04 02 18 08 00 ff 51 03 08 52 ae 00 ff 2f 00 4d 54 72 6b 00 00 00 33 00 ff 03 15 45 6c 65 63 2e 20 50 69 61 6e 6f 20 28 43 6c 61 73 73 69 63 29 00 c0 00 82 20 90 3c 32 60 80 3c 00 83 00 90 41 32 8d 40 80 41 00 00 ff 2f 00

作为图像:

首先,您的公式应该适用于您的简单情况。请看我的 answer to another question (Midi timestamp in seconds)。那里的公式是微秒的,所以我们可以将它除以 1000,我们将得到你的毫秒计算结果。

嗯,我已经在我的图书馆检查过你的文件。首先我要注意的是你是对的,第二个音符长约 2 秒:

现在让我们将您的公式应用于第二个音符的节拍长度:

嗯...这显然是错误的数字。但是修复非常容易。这行你的公式问题:

double bpm =  (60000000 / mTempo);

您将 bpm 定义为 double,但您没有使用 double。事实上,你有整数除法,因此你失去了导致糟糕结果的精度。让我们检查新代码:

太棒了!我们现在有正确的号码。但我在这里没有看到 7000。我想你的 VLQ 计算是错误的。我的意思是这一行:

int deltaTicks = Math.abs(calculateDeltaTicks(getVariableLengthQuantity()));

这里是正确的时间和第二个音符的长度,所以请检查你的数字:

但在一般情况下,您的方法是错误的。它会在大多数 MIDI 文件上失败。您的公式使用两个主要假设:

  • 文件中没有速度变化;
  • 在 Note On 和相应的 Note Off 之间没有其他 MIDI 事件。

这两种假设在现实世界中都不成立。要正确计算以毫秒为单位的音符长度,您需要:

  • 计算 Note On 事件的时间(以毫秒为单位);
  • 以毫秒为单位计算相应音符关闭事件的时间;
  • 取这些时间的差值。

如果没有速度变化,时间的计算很简单。您只需要计算一个事件的 绝对时间 加上所有 delta-times 之前的事件,然后应用您的公式。

但是随着节奏的变化计算时间需要更复杂的逻辑。您需要观察恒定速度的每个范围,并沿途将每个速度的长度相加(以毫秒为单位),直到 MIDI 事件的时间落入这些范围之一。

您正在使用 Java,因此我无法为该任务推荐合适的工具。对于 .NET,您可以使用我的 DryWetMIDI 库,它支持不同格式的时间和长度,可以正确处理速度变化。