MidiMessageSequence -> MidiFile question

I’m trying to store some incoming midi data via handleIncomingMidiMessage() in a midiMessageSequence and then write the midi file to the disk after a period of time. The problem i’m running into is when I compare my midi file vs. what gets captured when I record simultaneously in Logic.

void handleIncomingMidiMessage( ..., const MidiMessage& message ) {
  MidiMessage m = message;
//I wasn't getting any differentiation between events in the saved midi file until I added this line:
  m.setTimeStamp( Time::getMillisecondCounterHiRes() );  //notice the lack of "* 0.001";
  mms.addEvent(m);
}

…after 30 seconds of playing or so…

void timerCallback() {
  MidiFile mf;
  mf.setTicksPerQuarterNote(960);
  //mf.setSmpteTimeFormat(25, 40); //this line causes the midi file to not load at all into Logic
  mms.addEvent(MidiMessage::tempoMetaEvent((60000 / 96) * 1000 ));
  mms.updateMatchedPairs();
  mms.sort();
  mf.addTrack(mms);
  //mf.convertTimestampTicksToSeconds(); //this line causes things to not render at all
   
  FileChooser fc("Choose a midi File",
                               File::getSpecialLocation(File::userHomeDirectory),
                               "*.mid;",
                               true);
                ;
  if( fc.browseForFileToSave(true) ) {
    File result = fc.getResult();
    DBG("result: " + String( result.getFullPathName() ) );
    //write our midiFile to this stream
    if( result.create().wasOk() != true ) {
      DBG( "couldn't create file" );
      return;
    }
    if( result.existsAsFile() ) {
      result.deleteFile();
      //try to write our data to this file
      ScopedPointer<FileOutputStream> fos = result.createOutputStream();
       midiFile.writeTo(*fos);
      DBG( "fileSize: " + String( result.getSize()) );
    }
  }
}

Any idea what’s going on with the difference in results when I drag in the saved midi file into Logic and compare it against what Logic recorded?

2 Likes

From what i can tell, it’s the mf.setTicksPerQuarterNote(); parameter that affects timing the most.
240 makes it twice as long as Logic’s interpretation
480 is about 125% longer
640 is ever so slightly quicker than Logic
960 is what you see in the picture.

I don’t understand why this is happening tho. Any insight?

FWIW: Logic’s resolution is fixed at 960 ticks per quarter note: https://discussions.apple.com/thread/488292?start=0&tstart=0

ok, i am just not understanding the problem with my code for some reason. Here’s a pic of Logic’s data being recorded at the same time as when I use my little midi recorder. You can see the SMPTE times of all of the events, the lengths, and also the SMPTE FPS, which is 50. I set it to 50, so that there are 20ms per frame. that makes it nice and easy to convert a SMPTE time of, say, 00:00:00:25.46 into milliseconds by multiplying 25.46 * 20 = 509.2ms, which makes sense. If you’re doing 50FPS, and the time stamp says you’re at frame 25.46 out of 50, then you’re roughly around 500ms.

Ok, so here is my code and the output from that code that corresponds with the midi shown in the picture above:

class MidiRecorder : public Timer, public MidiInputCallback {
    public: 
    MidiRecorder() {
    //    double msPerQuarterNote = (60000.f / 96.f); //96bpm
    //    double ticksPerQuarterNote = 960;
        msPerTick = (60000.f / 96.f) / 960.f;
        saveTimerIsRunning = false;
        enableAllMidiInputs();
    }
    void handleIncomingMidiMessage(juce::MidiInput *source, const juce::MidiMessage &message) override {   
      if( !saveTimerIsRunning ) {
            //startTimer(5 * 1000 * 60 );
            startTime = Time::getMillisecondCounterHiRes();
            startTimer(10000);//10 seconds till File:Save dialog pops up
            saveTimerIsRunning = true;
            DBG( "msPerTick: " + String(msPerTick));
      }
      mms.addEvent(message);
      mms.getEventPointer(mms.getNumEvents()-1)->message.setTimeStamp(Time::getMillisecondCounterHiRes() - startTime);
      DBG( "event Time: "
        + String(mms.getEventTime(mms.getNumEvents()-1))
        + " "
        + mms.getEventPointer(mms.getNumEvents()-1)->message.getDescription());
    }

    void MidiRecorder::timerCallback() {
      stopTimer();
      MidiFile mf;
      mf.setTicksPerQuarterNote(960);
      int microsecondsPerQuarter = 60.0*Time::getHighResolutionTicksPerSecond()/96; //96 is the BPM
      MidiMessage tempoEvent = MidiMessage::tempoMetaEvent(microsecondsPerQuarter);
      tempoEvent.setTimeStamp( startTime );
      mms.addEvent(tempoEvent); //tempo is 96bpm

      mms.updateMatchedPairs();
      mms.sort();
      mf.addTrack(mms);
      ... code to save the MidiFile to disk;
    }
    private:
      MidiMessageSequence mms;
      double startTime;
      double msPerTick;
      bool saveTimerIsRunning = false;
    };

Here’s the output I get from the DBG() in handleIncomingMidiMessage:

msPerTick: 0.651042
event Time: 0.131516 Note on C1 Velocity 90 Channel 1
event Time: 526.812 Note off C1 Velocity 0 Channel 1
event Time: 540.458 Note on D1 Velocity 71 Channel 1
event Time: 1047.9 Note off D1 Velocity 0 Channel 1
event Time: 1054.01 Note on E1 Velocity 75 Channel 1
event Time: 1588.53 Note off E1 Velocity 0 Channel 1
event Time: 1593.19 Note on F1 Velocity 67 Channel 1
event Time: 2154.84 Note on G1 Velocity 67 Channel 1
event Time: 2156.5 Note off F1 Velocity 0 Channel 1
event Time: 2695.46 Note off G1 Velocity 0 Channel 1

Let’s do some math for the durations between noteOns and noteOffs since the data is in milliseconds:

    C1: 526.680ms
    D1: 507.442ms
    E1: 534.52ms
    F1: 563.31ms
    G1: 540.62ms

Now, let’s look at the durations provided by logic in the picture. Again, 50FPS, so we multiply the event duration * 20ms to get the duration of the event in ms.

C1: 525.4ms
D1: 506.2ms
E1: 531.6ms
F1: 563.0ms
G1: 540.2ms

Ok cool, it’s not too far off from the info spit out by the DBG() messages.

Now, when I write the midi data into a Midi File, that’s not what gets written! I end up with these values instead:


multiplying them by 20ms, we get:

C1: 342.4ms
D1: 328.6ms
E1: 346.6ms
F1: 365.2ms
G1: 349.2ms

The difference between what gets calculated and what gets written by JUCE is as follows:

C1 difference: 65.16%
D1 difference: 64.91%
E1 difference: 65.19%
F1 difference: 64.86%
G1 difference: 64.64%

So, somewhere in the timerCallback() method the timeStamp for each midiMessage is being reduced by ~65%.

What happens if I change the TicksPerQuarterNote to be 960 * 0.65?

Here’s midi recorded by logic (note the length/info column on the right)

and here’s the midi file generated by my timerCallback(). note the length/info column on the right!!!

@jules @ed95 or any other JUCE devs, could you comment on this odd behavior when writing MidiFiles to disk?

Can you post the 2 MIDI files created (Logic and JUCE)?

Rail

Ok, it seems the problem is the following line, which i commented out and replaced with actual microsecond calculations:

//int microsecondsPerQuarter = 60.0*Time::getHighResolutionTicksPerSecond()/96.f; //96 is the BPM
int microsecondsPerQuarter = (60000.f / 96.f) * 1000.f;

I don’t understand why, because the commented line generated 6250000, and the uncommented line generated 62500. that’s not a difference of 65%

I can toss in any bpm in that equation and it works fine now.

@jules @ed95 I think for simplifying the trouble users have been going thru (there are a lot of posts about writing midi files correctly), can we get a MidiMessage:tempoMetaEvent(float tempoBPM) added to the codebase?

MidiMessage MidiMessage::tempoMetaEvent(const float& tempoBPM) {
  int microsecondsPerQuarternote = (60000.f / (float)tempoBPM) * 1000.f;
  return MidiMessage::tempoMetaEvent(microsecondsPerQuarternote);
}

@Rail_Jon_Rogut I think I figured out the problem. see my post below. Let me know if I still need to upload them. it seems really solid now with this minor change.

If you think you have it sussed out no worries… I was just going to open them in MIDIKit to see if there was anything obvious.

Cheers,

Rail

2 Likes

It’s relatively easy to convert between BPM and microseconds per quarter note using that equation so I’m not sure a function to do it in MidiMessage is necessary.

The problem that you’re having with timing comes from the fact that the timestamp of a MidiMessage added to a MidiFile needs to be in ticks not time, so when you receive a midi message in your callback you need to convert the time elapsed from a given start time to ticks. The number of ticks per quarter note is set using the MidiFile::setTicksPerQuarterNote() method so you need to multiply this by how many quarter notes have elapsed since the start time to get your timestamp.

Hope this helps!
Ed

Can we get added to the documentation that those timestamps need to be in ticks? That would have solved the whole problem. Also, my events were being added with the timestamp in terms of seconds, not ticks.

mms.getEventPointer(mms.getNumEvents()-1)->message.setTimeStamp(Time::getMillisecondCounterHiRes() - startTime);

so, to claim that they need to be added in terms of ticks is false. I was storing them in terms of seconds and never converted them to ticks and it all worked just fine. So, I don’t know how to explain it, but some things in the documentation and codebase just don’t line up with your response. Any chance we could get some clarification?

If you take a look at this line in MidiFile.cpp, you can see that timestamps in the MidiFile object are stored in ticks. When you load this file into Logic, it uses the number of ticks per quarter note that you set using the setTicksPerQuarterNote() method and the tempo meta event of the MIDI file to place the MIDI message events on the timeline. So if the timestamps of the MidiMessage objects are in seconds, or milliseconds in your case, then the events won’t be placed correctly on the timeline, hence your timing errors.

Ed

mms.getEventPointer(mms.getNumEvents()-1)->message.setTimeStamp(Time::getMillisecondCounterHiRes() - startTime);

to claim that they need to be added in terms of ticks is false. I was storing them in terms of seconds and never converted them to ticks and it all worked just fine

This is what that getTimestamp() method does. In my code, I’m setting the time stamp in seconds as a double and the line you linked to is actually just truncating off the decimal part.

//==============================================================================
/** Returns the timestamp associated with this message.
The exact meaning of this time and its units will vary, as messages are used in
a variety of different contexts.
If you’re getting the message from a midi file, this could be a time in seconds, or
a number of ticks - see MidiFile::convertTimestampTicksToSeconds().
If the message is being used in a MidiBuffer, it might indicate the number of
audio samples from the start of the buffer.
If the message was created by a MidiInput, see MidiInputCallback::handleIncomingMidiMessage()
for details of the way that it initialises this value.
@see setTimeStamp, addToTimeStamp
*/
double getTimeStamp() const noexcept { return timeStamp; }

Here in MidiMessageSequence is what happens when an event is added:

MidiEventHolder* const newOne = new MidiEventHolder (newMessage);
timeAdjustment += newMessage.getTimeStamp();
newOne->message.setTimeStamp (timeAdjustment);

I’m on my phone so sorry for lack of formatting… What is ‘timeAdjustment’ in this context?

Oh? timeAdjustment is an optional double param initialized to 0.

Hmmmmm… So did I happen to play midi notes from my keyboard that the timestamp in terms of milliseconds is identical to ticks??

ok, so just to help someone else out:
in your constructor/class def:

double startTime = Time::getMillisecondCounterHiRes();
msPerTick = (60000.f / tempo) / 960.f; //960 ticks per quarternote

in your handleIncomingMidiMessage():

double timeStampInMS = Time::getMillisecondCounterHiRes() - startTime;
m.setTimeStamp(timeStampInMS / msPerTick);

in your MidiMessageSequence populator:

int microsecondsPerQuarter = (60000.f / tempo) * 1000.f;
MidiMessage tempoEvent = MidiMessage::tempoMetaEvent(microsecondsPerQuarter);
tempoEvent.setTimeStamp( 0 );
MidiMessageSequence mms;
mms.addEvent(tempoEvent);

in your midiFile generator:

midiFile.setTicksPerQuarterNote(960);
midiFile.addTrack(mms);
2 Likes