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??
