MidiBuffer --> MidiMessageSequence issue

I’m working on generating MIDI and exporting to a .mid file. I see there’s been a few topics similar to this, but none of them seem to mention this exact issue. Feel like I’m going a little crazy here…

What I’m basically doing is generated some MidiMessages into a MidiBuffer, and then turning that into a MidiMessageSequence (which I can then write to a .mid file). If I make a very simple, stupid version of this which manually writes 4 quarter notes in a row, everything works as expected. Code snippet:

        juce::MidiMessageSequence sequence;
        const int ticksPerQuarterNote = 960;
        const double bpm = 120.0;

        // Set tempo (in microseconds per quarter note)
        int mpqn = static_cast<int>(60.0 * 1000000.0 / bpm);
        sequence.addEvent(juce::MidiMessage::tempoMetaEvent(mpqn), 0);
        
        // Set time signature (4/4)
        sequence.addEvent(juce::MidiMessage::timeSignatureMetaEvent(4, 4), 0);

        // Create a 4-note sequence of C4 notes
        int noteNumber = 60; // MIDI note for C4
        int noteVelocity = 100;
        int noteDurationInTicks = ticksPerQuarterNote; // A quarter note
        int numberOfNotes = 4;

        for (int i = 0; i < numberOfNotes; ++i)
        {
            int startTick = i * ticksPerQuarterNote;
            int endTick = startTick + noteDurationInTicks;
            
            // Add a Note On message
            juce::MidiMessage noteOn = juce::MidiMessage::noteOn(1, noteNumber, (juce::uint8)noteVelocity);
            sequence.addEvent(noteOn, startTick);
            
            DBG("noteOn i: " << i << ", Time in Ticks: " << startTick);

            // Add a Note Off message
            juce::MidiMessage noteOff = juce::MidiMessage::noteOff(1, noteNumber);
            sequence.addEvent(noteOff, endTick);
        }
        
        
        DBG ("sequence start: " + juce::String(sequence.getStartTime()));
        DBG ("sequence end: " + juce::String(sequence.getEndTime()));
        for (auto sequenceMessage : sequence)
        {
            DBG (sequenceMessage->message.getDescription());
        }
        
        juce::MidiFile midi;
        midi.setTicksPerQuarterNote(ticksPerQuarterNote);
        midi.addTrack(sequence);

If I examine the logs, I get pretty much exactly what you’d expect, and it imports fine into Ableton or Bitwig. Logs:

noteOn 0, Time in Ticks: 0

noteOn 1, Time in Ticks: 960

noteOn 2, Time in Ticks: 1920

noteOn 3, Time in Ticks: 2880

sequence start: 0

sequence end: 3840

Meta event

Meta event

Note on C3 Velocity 100 Channel 1

Note off C3 Velocity 0 Channel 1

Note on C3 Velocity 100 Channel 1

Note off C3 Velocity 0 Channel 1

Note on C3 Velocity 100 Channel 1

Note off C3 Velocity 0 Channel 1

Note on C3 Velocity 100 Channel 1

Note off C3 Velocity 0 Channel 1

Now, the issue comes when I actually translate from a MidiBuffer to a MidiMessageSequence. I’ve set things up so that I similarly just write 4 quarter notes into the MidiBuffer. However, once I turn that into the MidiMessageSequence, all the timing gets super screwy. In addition, when I import it into Ableton or Bitwig, the output is stretched over the course of many bars (~30).

Code:

        juce::MidiMessageSequence sequence;
        const int ticksPerQuarterNote = 960;
        const double bpm = 120.0;

        // Set tempo (in microseconds per quarter note)
        int mpqn = static_cast<int>(60.0 * 1000000.0 / bpm);
        sequence.addEvent(juce::MidiMessage::tempoMetaEvent(mpqn), 0);
        
        // Set time signature (4/4)
        sequence.addEvent(juce::MidiMessage::timeSignatureMetaEvent(4, 4), 0);
        
        double samplesPerQuarterNote = (60.0 / playbackInfo.bpm) * playbackInfo.sampleRate;
        double ticksPerSample = ticksPerQuarterNote / samplesPerQuarterNote;

        // Iterate through the MidiBuffer and convert sample positions to ticks
        for (const auto metadata : tempBuffer)
        {
            // Convert sample position to ticks
            int timeInTicks = static_cast<int>(metadata.samplePosition * ticksPerSample);
            DBG("Sample Position: " << metadata.samplePosition << ", Time in Ticks: " << timeInTicks);

            sequence.addEvent(metadata.getMessage(), timeInTicks);
        }
        
        DBG ("sequence start: " + juce::String(sequence.getStartTime()));
        DBG ("sequence end: " + juce::String(sequence.getEndTime()));
        for (auto sequenceMessage : sequence)
        {
            DBG (sequenceMessage->message.getDescription());
        }
        
        juce::MidiFile midi;
        midi.setTicksPerQuarterNote(ticksPerQuarterNote);
        midi.addTrack(sequence);

And the logs yield the following:

Sample Position: 0, Time in Ticks: 0

Sample Position: 24000, Time in Ticks: 960

Sample Position: 48000, Time in Ticks: 1920

Sample Position: 72000, Time in Ticks: 2880

sequence start: 0

sequence end: 99823

Meta event

Meta event

Note on C3 Velocity 101 Channel 1

Note off C3 Velocity 0 Channel 1

Note on C3 Velocity 101 Channel 1

Note off C3 Velocity 0 Channel 1

Note on C3 Velocity 101 Channel 1

Note off C3 Velocity 0 Channel 1

Note on C3 Velocity 101 Channel 1

Note off C3 Velocity 0 Channel 1

For some reason, this sequence is muchhhhhh longer, even though I’ve set up its tempo, time signature, and PPQ the exact same way, and I validated that we’re adding the event to the MidiMessageSequence at the exact same points.

Can anyone help me figure out why this would possibly be any different? As far as I can tell, the only real difference is that MidiMessageSequence::addEvent() is being called with a fresh MidiMessage versus pulling it out of the MidiBuffer. AFAIK, the MidiMessage itself has no timing data either - the only timing data is from MidiMessageMetadata::samplePosition, so using MidiMessageMetadata::getMessage() means that the message should be virtually the same as making a fresh message via MidiMessage::noteOn… right??

I think I’ve figured this out. I suppose it’s the old adage of asking tech support and figuring it out moments later :slight_smile:

the only timing data is from MidiMessageMetadata::samplePosition

This is incorrect. The MidiMessage has an inherent timestamp, which can be viewed via getTimeStamp. It seems like MidiMessageSequence::addEvent looks at the message’s timestamp, and treats is as a PPQ value (rather than a sample position, which is where it originates). So my messages had timestamps such as 23984, 24000, 47984, etc. - causing a very long sequence if you consider those values to be PPQs.

So the correct implementation is something like this:

// Iterate through the MidiBuffer and convert sample positions to ticks
for (const auto metadata : tempBuffer)
{
    // Convert sample position to ticks
    int timeInTicks = static_cast<int>(metadata.samplePosition * ticksPerSample);
    DBG("Sample Position: " << metadata.samplePosition << ", Time in Ticks: " << timeInTicks);
    
    DBG("msg timestamp: " + juce::String(metadata.getMessage().getTimeStamp()));

    sequence.addEvent(metadata.getMessage().withTimeStamp(timeInTicks));
}