如何使用 Nadio 获取 midi 事件的实时时间

Posted

技术标签:

【中文标题】如何使用 Nadio 获取 midi 事件的实时时间【英文标题】:How to get the real time of a midi event using Naudio 【发布时间】:2020-03-31 22:45:25 【问题描述】:

我正在尝试使用 NAudio 库阅读 MIDI 音符并提取每个音符的实时时间 我写了这段代码,但它没有正确计算时间,我使用了一个我找到的公式here ((note.AbsTime - lastTempoEvent.AbsTime) / midi.ticksPerQuarterNote) * tempo + lastTempoEvent.RealTime

代码:

    var strictMode = false;
    var mf = new MidiFile("Assets/Audios/Evangelion Midi.mid", strictMode);
    mf.Events.MidiFileType = 0;
    List<MidiEvent> midiNotes = new List<MidiEvent>();
    List<TempoEvent> tempoEvents = new List<TempoEvent>();


    for (int n = 0; n < mf.Tracks; n++)
    
        foreach (var midiEvent in mf.Events[n])
        
            if (!MidiEvent.IsNoteOff(midiEvent))
            
                midiNotes.Add(midiEvent);

                TempoEvent tempoE;
                try  tempoE = (TempoEvent)midiEvent; tempoEvents.Add(tempoE);

                    Debug.Log("Absolute Time " + tempoE.AbsoluteTime);

                
                catch  


            
        
    

    notesArray = midiNotes.ToArray();
    tempoEventsArr = tempoEvents.ToArray();

    eventsTimesArr = new float[notesArray.Length];
    eventsTimesArr[0] = 0;

    for (int i = 1; i < notesArray.Length; i++)
    
        ((notesArray[i].AbsoluteTime - tempoEventsArr[tempoEventsArr.Length - 1].AbsoluteTime) / mf.DeltaTicksPerQuarterNote)
            * tempoEventsArr[tempoEventsArr.Length - 1].MicrosecondsPerQuarterNote + eventsTimesArr[i-1];


    

我得到了这些values 显然不正确

有人说错了吗?

【问题讨论】:

可能会失败的一件事是当(TempoEvent)midiEvent 失败并抛出InvalidCastException 时,midiNotestempoEvents 之间的元素计数将不同。它最终会抛出 OutOfIndexException 。但这可能不是问题。我认为您应该逐轨解码它,而不是将它们相互连接 @JeroenvanLangen 他正在设置mf.Events.MidiFileType = 0; 然后像只有一个轨道的 0 型 midi 文件一样检索事件。 NAudio 在设置为零时调用FlattenToOneTrack(); 请将 try/catch 块更改为 if(midiEvent is TempoEvent)。通过这样做,您将获得非常、非常、非常巨大的性能提升。 【参考方案1】:

在这里看到有人喜欢 MIDI 真是太好了。

引用代码中的note.AbsTime - lastTempoEvent.AbsTime 部分在您这边实现不正确。

此代码中的lastTempoEvent 变量不能表示midi 文件中的最后一个速度变化(因为您已经使用notesArray[i].AbsoluteTime - tempoEventsArr[tempoEventsArr.Length - 1].AbsoluteTime 实现了它)。

引用的代码试图做的是获取当前 note 时的速度(可能通过将最后出现的速度变化事件存储在此变量中),而您的代码正在减去绝对时间整个midi文件中的最新速度变化。这就是负数的根本原因(如果当前音符后有任何节奏变化)。

旁注:我还建议保留注释事件的时间安排。如果您不知道何时发布便笺,您如何关闭它? 试试这个。我测试了它并且它有效。请仔细阅读内联 cmets。

注意安全。

static void CalculateMidiRealTimes()

    var strictMode = false;
    var mf = new MidiFile("C:\\Windows\\Media\\onestop.mid", strictMode);
    mf.Events.MidiFileType = 0;

    // Have just one collection for both non-note-off and tempo change events
    List<MidiEvent> midiEvents = new List<MidiEvent>();

    for (int n = 0; n < mf.Tracks; n++)
    
        foreach (var midiEvent in mf.Events[n])
        
            if (!MidiEvent.IsNoteOff(midiEvent))
            
                midiEvents.Add(midiEvent);

                // Instead of causing stack unwinding with try/catch,
                // we just test if the event is of type TempoEvent
                if (midiEvent is TempoEvent)
                
                    Debug.Write("Absolute Time " + (midiEvent as TempoEvent).AbsoluteTime);
                
            
        
    

    // Now we have only one collection of both non-note-off and tempo events
    // so we cannot be sure of the size of the time values array.
    // Just employ a List<float>
    List<float> eventsTimesArr = new List<float>();

    // we introduce this variable to keep track of the tempo changes
    // during play, which affects the timing of all the notes coming
    // after it.
    TempoEvent lastTempoChange = null;

    for (int i = 0; i < midiEvents.Count; i++)
    
        MidiEvent midiEvent = midiEvents[i];
        TempoEvent tempoEvent = midiEvent as TempoEvent;

        if (tempoEvent != null)
        
            lastTempoChange = tempoEvent;
            // Remove the tempo event to make events and timings match - index-wise
            // Do not add to the eventTimes
            midiEvents.RemoveAt(i);
            i--;
            continue;
        

        if (lastTempoChange == null)
        
            // If we haven't come accross a tempo change yet,
            // set the time to zero.
            eventsTimesArr.Add(0);
            continue;
        

        // This is the correct formula for calculating the real time of the event
        // in microseconds:
        var realTimeValue =
            ((midiEvent.AbsoluteTime - lastTempoChange.AbsoluteTime) / mf.DeltaTicksPerQuarterNote)
            *
            lastTempoChange.MicrosecondsPerQuarterNote + eventsTimesArr[eventsTimesArr.Count - 1];

        // Add the time to the collection.
        eventsTimesArr.Add(realTimeValue);

        Debug.WriteLine("Time for 0 is: 1", midiEvents.ToString(), realTimeValue);
    


编辑:

计算实际时间时的除法是 int/float,当事件之间的刻度小于每个四分音符的增量刻度时,结果为零。

这是使用精度最好的数字类型小数计算值的正确方法。

midi 歌曲 onestop.mid İS 4:08(248 秒)长,我们​​的最终事件实时是 247.3594906770833

static void CalculateMidiRealTimes()

    var strictMode = false;
    var mf = new MidiFile("C:\\Windows\\Media\\onestop.mid", strictMode);
    mf.Events.MidiFileType = 0;

    // Have just one collection for both non-note-off and tempo change events
    List<MidiEvent> midiEvents = new List<MidiEvent>();

    for (int n = 0; n < mf.Tracks; n++)
    
        foreach (var midiEvent in mf.Events[n])
        
            if (!MidiEvent.IsNoteOff(midiEvent))
            
                midiEvents.Add(midiEvent);

                // Instead of causing stack unwinding with try/catch,
                // we just test if the event is of type TempoEvent
                if (midiEvent is TempoEvent)
                
                    Debug.Write("Absolute Time " + (midiEvent as TempoEvent).AbsoluteTime);
                
            
        
    

    // Switch to decimal from float.
    // decimal has 28-29 digits percision
    // while float has only 6-9
    // https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/floating-point-numeric-types

    // Now we have only one collection of both non-note-off and tempo events
    // so we cannot be sure of the size of the time values array.
    // Just employ a List<float>
    List<decimal> eventsTimesArr = new List<decimal>();

    // Keep track of the last absolute time and last real time because
    // tempo events also can occur "between" events
    // which can cause incorrect times when calculated using AbsoluteTime
    decimal lastRealTime = 0m;
    decimal lastAbsoluteTime = 0m;

    // instead of keeping the tempo event itself, and
    // instead of multiplying every time, just keep
    // the current value for microseconds per tick
    decimal currentMicroSecondsPerTick = 0m;

    for (int i = 0; i < midiEvents.Count; i++)
    
        MidiEvent midiEvent = midiEvents[i];
        TempoEvent tempoEvent = midiEvent as TempoEvent;

        // Just append to last real time the microseconds passed
        // since the last event (DeltaTime * MicroSecondsPerTick
        if (midiEvent.AbsoluteTime > lastAbsoluteTime)
        
            lastRealTime += ((decimal)midiEvent.AbsoluteTime - lastAbsoluteTime) * currentMicroSecondsPerTick;
        

        lastAbsoluteTime = midiEvent.AbsoluteTime;

        if (tempoEvent != null)
        
            // Recalculate microseconds per tick
            currentMicroSecondsPerTick = (decimal)tempoEvent.MicrosecondsPerQuarterNote / (decimal)mf.DeltaTicksPerQuarterNote;

            // Remove the tempo event to make events and timings match - index-wise
            // Do not add to the eventTimes
            midiEvents.RemoveAt(i);
            i--;
            continue;
        

        // Add the time to the collection.
        eventsTimesArr.Add(lastRealTime);

        Debug.WriteLine("Time for 0 is: 1", midiEvent, lastRealTime / 1000000m);
    
 

【讨论】:

非常感谢您的回答!您的代码很干净,并且 cmets 非常有见地。只有一件事,我使用的midi文件长243秒,但最后一个以秒计算的realTimeValue是115秒(我划分了var realTimeValue / 1000000f),这是正常的还是我错过了什么?这是最后的结果 [链接] (imgur.com/Tg7xcMB) 嗨。你说的对。代码中存在错误并且计算失败,因为我们将 int 划分为 float 并且对于小的 delta 时间值,这会导致实时增量为零。我通过使用decimal 纠正了这个问题,并更改了代码以使其更简单。编辑答案 不客气。顺便说一句,你在实施什么?你是要播放这些 MIDI 文件,还是只是研发 哦,我正在 Unity 中制作节奏游戏(Think Guitar Hero、Piano Tiles 等)。现在我要使用它们的 realTimeValue 来生成笔记。剩下的就是弄清楚他们的速度和游戏循环 哇!现在太好了。 MIDI 的实际使用:) 我喜欢它。

以上是关于如何使用 Nadio 获取 midi 事件的实时时间的主要内容,如果未能解决你的问题,请参考以下文章

在 Java 中从 Receiver 获取 midi 消息

Mido - 如何从不同端口实时获取 midi 数据

从 MIDI 设备实时获取输入(Python)

如何从 MIDI 序列中获取 Note On/Off 消息?

获取麦克风输入电平 Nadio

可编程“实时”MIDI处理