-1

I am trying to read midi notes and extract the real time of each one of them using NAudio library I wrote this code but it isn't calculating the time correctly, I used a formula that i found here ((note.AbsTime - lastTempoEvent.AbsTime) / midi.ticksPerQuarterNote) * tempo + lastTempoEvent.RealTime

The code:

    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];


    }

I got these values which are clearly not correct

Does anyone where I am wrong?

Wazowski
  • 1
  • 1
  • One thing that could fail is when the `(TempoEvent)midiEvent` fails and an InvalidCastException throws, the count of elements will differ between `midiNotes` and `tempoEvents`. It will throw an OutOfIndexException eventually. But that is probably not the problem. I think you should decode it track by track instead of them concatenated to each other – Jeroen van Langen Mar 31 '20 at 23:15
  • @JeroenvanLangen he is setting `mf.Events.MidiFileType = 0;` then the events are retrieved like a type 0 midi file which has only one track. NAudio calls `FlattenToOneTrack();` when this is set to zero. – Oguz Ozgul Apr 01 '20 at 00:11
  • Please change the try/catch block to `if(midiEvent is TempoEvent)`. You will get a very, very, very huge performance gain by doing so. – Oguz Ozgul Apr 01 '20 at 00:12

1 Answers1

1

It is really good to see someone into MIDI here.

The note.AbsTime - lastTempoEvent.AbsTime part in the referenced code is implemented incorrectly on your side.

The lastTempoEvent variable in this code can not mean the last tempo change in the midi file (as you've implemented it using notesArray[i].AbsoluteTime - tempoEventsArr[tempoEventsArr.Length - 1].AbsoluteTime).

What the referenced code is trying to do is to get the tempo at the time of the current note, (probably by storing the last appeared tempo change event in this variable) while your code is subtracting the absolute time of the latest tempo change in the whole midi file. This is the root cause of the negative numbers (if there are any tempo changes after the current note).

Side note: I also recommend keeping the timings of note-off events. How do you close a note if you don't know when it is released? Try this. I tested it and it works. Please read the inline comments carefully.

Be safe.

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);
    }

}

EDIT:

The division while calculating the real times was an int/float which resulted in zero when the ticks between events are smaller than delta ticks per quarter note.

Here is the correct way to calculate the values using the numeric type decimal which has the best precision.

The midi song onestop.mid İS 4:08 (248 seconds) long and our final event real time is 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://learn.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);
    }
} 
Oguz Ozgul
  • 6,809
  • 1
  • 14
  • 26
  • Thank you so much for your answer! Your code is clean and the comments are very insightful. There is just one thing, the midi file I'm using is 243sec long but The last realTimeValue computed in seconds is 115sec (I divided var realTimeValue /1000000f), is it normal or am I missing someting? here are the last results [link] (https://imgur.com/Tg7xcMB) – Wazowski Apr 01 '20 at 02:44
  • Hi. You are right. There is an error in the code and the calculation fails because we divide int to float and for small delta time values this causes a zero increment in the real time. I corrected this by using `decimal` and also changed the code to make it simpler. Editing answer – Oguz Ozgul Apr 01 '20 at 07:54
  • You're very welcome. What are you implementing by the way? Are you going to play these MIDI files, or is it just research&development – Oguz Ozgul Apr 01 '20 at 12:44
  • Oh I'm making a rhythm game in Unity (Think Guitar Hero, Piano Tiles and such). Now I'm gonna make the notes spawn using their realTimeValue. All is left is to figure out their speed and the game loop – Wazowski Apr 01 '20 at 13:03
  • Wow! now that's great. Practical use of MIDI :) I loved it. – Oguz Ozgul Apr 01 '20 at 14:13
  • I just tried this, but returns significantly wrong time compared to what Cakewalk shows. This calculation returns 16 min, but it is actually 5 min. Here is the file: https://www.midiworld.com/download/3476 – Soli Aug 12 '21 at 03:21