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