Search code examples
c#eventsnaudiotempo

Midi real time after Tempo event using NAudio


I am trying to calculate real time of midi note on events using NAudio out of absolute time given. I am using the following code:

private static double CalcFactor(long noteAbsTime, long lastTempoAbsTime, int ticksPerQuarterNote, int tempo, double lastTempoRealTime)  //calculate the factor needed for presenting time in seconds
        {
            double currentTempoRealTime =
                ((double)((double)(noteAbsTime - lastTempoAbsTime) /
                          (double)ticksPerQuarterNote) * (double)tempo + lastTempoRealTime) / 1000000D;
            return currentTempoRealTime;
        }

but there is no continuity in real time after a tempo event. What is the correct formula with which I can have the real time in seconds instead of delta ticks of absolute time, for the case of multiple tempo events in a midi file?


Solution

  • [deleted prior answer due to way too many edits]

    "MBT"

    // Time-sig with 4/4 assumed (as opposed to perhaps 3/4)
    internal static string GetMBT(long pulse, int division)
    {
      double value = (double)pulse;
      var M = Convert.ToInt32(Math.Floor(value / (division * 4.0)) + 1);
      var B = Convert.ToInt32((Math.Floor(value / division) % 4) + 1);
      var T = pulse % division;
      return string.Format(fmt3, M, B, T);
    }
    

    MidiEvent.AbsoluteTime represents the number of Ticks or Pulses as this would just tell us what can otherwise be shown as MBT (Measure, Bar [maybe beats] and Ticks).

    e.g. 0 = 001:01:000 (M:B:T) or 00:00:000 depending on what you prefer (and we removed the +1 from the above function).

    Tempo Map (or State) Calculation(s)

    internal static double GetSeconds(int division, double tempo, long pulse, double sec = 0.0)
    {
      return ((60.0 / tempo) * ((double)(pulse) / division)) + sec;
    }
    internal static string GetSSeconds(double seconds)
    {
      var T = TimeSpan.FromSeconds(seconds);
      return string.Format("{0:00}:{1:00}:{2:00}.{3:00000}", T.Hours, T.Minutes, T.Seconds, T.Milliseconds);
    }
    

    If no tempo event is set in the MIDI file, 120BPM is assumed
    e.g.: 500000 microseconds per quarter note (60000000.0 / 500000 = 120)

    Generally, we just want to maintain a tempo-state or something like that which will contain (or maybe be initialized as)

    • long mLastTempoPulses = 0
    • double mLastTempoSecond = 0.0
    • double mLastTempoValue = 120 or the first SET_TEMPO message-value.

    When we encounter tempo-change from a TempoEvent (nTempo in the following snippet) we can say something like…

    // { some loop
    
    double mNewSecond = GetSeconds(
      miditrack.DeltaTicksPerQuarterNote,
      mLastTempoValue,
      nTempo.AbsoluteTime - mLastTempoPulses,
      mLastTempoSecond);
    
    // then we would continue to set the back-ref values
    mLastTempoPulses = nTempo.AbsoluteTime;
    mLastTempoValue  = nTempo.Tempo;
    mLastTempoSecond = mNewSecond;
    
    // ... }
    

    The above assumes that we're iterating through TempoEvent messages, however we would apply the same general concept to NoteEvent without the need for storing the back-reference value(s).

    Example GIST

    The the GIST (below) we crate a tempo-map generally using the above concept where tempo map is basically a list of

    class NewTempo
    {
      public long PulseMin, PulseMax;
      public double Seconds, Tempo;
      public bool Match(MidiEvent pnote) {
        return (pnote.AbsoluteTime >= PulseMin) && (pnote.AbsoluteTime < PulseMax);
      }
    }
    

    I've posted a example gist command-line program however don't have any software that proves or validates that it is indeed correct at the moment.

    you would probably want to output to a text file with it if you compile it since its output will likely surpass the console's buffer-size.

    app.exe file.mid > output.txt
    

    This could be done differently depending on if the use-case is reading input from a MidiFile or from incoming messages in real-time. The example GIST reads a midi file and generates a tempo-map that can be referenced, then it looks at notes in a given track (the first track containing notes).

    some output from the GIST example...

    it may look like a gap towards the first tempo change, however that's correct as there are no notes in the particular track.

    TEMPOEVENT (INPUT DATA)
    =======================
    
     -> 001:01:000 => Time=        0, Tempo=123.999991733334
     -> 095:01:000 => Time=    45120, Tempo=122.999969250008
     -> 216:01:000 => Time=   103200, Tempo=122.999969250008
     -> 218:01:000 => Time=   104160, Tempo=122.999969250008
    
    YIELD?
    ======
    
     -> Range={001:01:000 - 095:01:000}, BPM=124.0000, SS=00:03:01.00935
     -> Range={095:01:000 - 216:01:000}, BPM=123.0000, SS=00:06:58.00033
     -> Range={216:01:000 - 218:01:000}, BPM=123.0000, SS=00:07:01.00936
    
    MIDI Format 1
    Looking in track at index: 1
    Processing 2694 events.
     -> 001:01:000 @00:03:01.00935 F#4  NoteOn 100
     -> 001:01:030 @00:03:02.00056 F#4 NoteOff 0
     -> 001:01:060 @00:03:02.00177 E4   NoteOn 100
     -> 001:01:090 @00:03:02.00298 E4  NoteOff 0
     -> 001:02:060 @00:03:02.00661 B3   NoteOn 100
     -> 001:02:090 @00:03:02.00782 B3  NoteOff 0
     -> 001:03:060 @00:03:03.00145 A3   NoteOn 100
     -> 001:03:090 @00:03:03.00266 A3  NoteOff 0
     -> 001:04:000 @00:03:03.00387 G3   NoteOn 100
     -> 001:04:060 @00:03:03.00629 G3  NoteOff 0
    
    ...
    
     -> 088:04:000 @00:05:51.00774 A3   NoteOn 100
     -> 088:04:060 @00:05:52.00016 A3  NoteOff 0
     -> 088:04:060 @00:05:52.00016 G3   NoteOn 100
     -> 089:01:000 @00:05:52.00258 G3  NoteOff 0
    tempoIndex = 1
      => 45120 <= 49920 < 45120 bpm=122.999969250008
     -> 105:01:000 @00:07:17.00545 F#4  NoteOn 100
     -> 105:01:030 @00:07:17.00667 F#4 NoteOff 0
     -> 105:01:060 @00:07:17.00789 E4   NoteOn 100
     -> 105:01:090 @00:07:17.00911 E4  NoteOff 0
     -> 105:02:060 @00:07:18.00277 B3   NoteOn 100
     -> 105:02:090 @00:07:18.00399 B3  NoteOff 0
     -> 105:03:060 @00:07:18.00765 A3   NoteOn 100
     -> 105:03:090 @00:07:18.00887 A3  NoteOff 0
    
    ...
    

    Some notes

    Tempos are (should be) stored in the first (0'th) track unless we're looking at MIDI Format 2 (perhaps a rare case).

    From what I'd observed, there is either one set-tempo message, or 3 or more where the last tempo-event acts much like the EOT message (NAudio.Midi.MidiEvent.IsEndTrack(...)) —which of course, there is always a EOT.

    The gist example might have more helpful notes.

    Supplemental

    internal static IEnumerable<T> MidiEventT<T>(MidiFile midi, int tkid = 0, int max=-1)
      where T : MidiEvent
    {
      int LIMIT = midi.Events[tkid].Count, counter=0;
      if ((max != -1) && (max < LIMIT)) LIMIT = max;
      for (int i = 0; i < midi.Events[tkid].Count; i++)
      {
        if (counter == LIMIT) break;
        T tmsg = midi.Events[tkid][i] as T;
        if (tmsg == null) continue;
        counter++;
        yield return tmsg;
      }
    }
    

    example usage of the above

    // all tempo events
    var tempos = new List<TempoEvent>(MidiEventT<TempoEvent>(midi));
    // all note events (the gist has a better example)
    
    // trackID is the track with events you want to grab, of course
    var notes = new List<NoteEvent>(MidiEventT<NoteEvent>(midi, trackID));
    
    // trackID is the track with events you want to grab, of course
    var allMidiEventsInTrack = new List<MidiEvent>(MidiEventT<MidiEvent>(midi, trackID));