Based on this 2016 post by @_pi, I was able to get a basic midi player working. Thanks _pi!
That was several years ago and I’m now wondering if there are better ways to accomplish the code below.
Here’s my code:
double totalSampleLength = 0;
std::vector<juce::MidiMessage> midiMessages;
int samplesPlayed = 0;
void initMidi(const juce::String filename)
{
juce::FileInputStream fileStream((juce::File(filename)));
juce::MidiFile midiFile;
midiFile.readFrom(fileStream);
midiFile.convertTimestampTicksToSeconds();
const juce::MidiMessageSequence *track = midiFile.getTrack(0);
for (int i = 0; i < track->getNumEvents(); i++)
{
juce::MidiMessage &m = track->getEventPointer(i)->message;
if (m.getMetaEventType() == -1) // Ignore events that aren't note on/off
{
midiMessages.push_back(m);
}
}
}
void prepareToPlay(int samplesPerBlockExpected, double sampleRate)
{
localSampleRate = sampleRate;
synth.setCurrentPlaybackSampleRate(sampleRate);
midiBuffer.clear();
auto samplePosition = 0;
for (int i = 0; i < midiMessages.size() / 2; i++)
{
auto onIndex = i * 2;
auto offIndex = onIndex + 1;
auto note = midiMessages[onIndex];
midiBuffer.addEvent(note, samplePosition); // Note On event
// Calc the next samplePosition based on the length of the note
samplePosition += (sampleRate * note.getTimeStamp());
midiBuffer.addEvent(midiMessages[offIndex], samplePosition); // Not Off event
}
totalSampleLength = samplePosition;
}
#pragma once
#include <JuceHeader.h>
class JuceMidiFilePlayer
{
public:
JuceMidiFilePlayer()
{
PlayMidifile = false;
}
~JuceMidiFilePlayer()
{
PlayMidifile = false;
}
void setMidiFileObject(const juce::MidiFile& newMidifile)
{
// Make sure the playback is stopped before loading a new file
StartStopMidiFile(false);
midifile = newMidifile;
FileLoaded = true;
tick_count = 0;
sample_count = 0;
last_tick = midifile.getLastTimestamp();
TPQ = midifile.getTimeFormat();
lastSampleOffset = 0.0;
trackEventStart.clear();
trackEventStart.insertMultiple(0, 0, midifile.getNumTracks());
setTempo(tempoBPM);
if (onLoad != nullptr)
onLoad();
DBG("MidiFile loaded.");
DBG("midifile.getLastTimestamp() = " << midifile.getLastTimestamp()); // last tick
DBG("midifile.getTimeFormat() = " << midifile.getTimeFormat()); // tpq
DBG("midifile.getNumTracks() = " << midifile.getNumTracks());
}
void LoadMidiFileFromURL(const URL& url)
{
jassert(processMidi != nullptr);
jassert(allNotesOff != nullptr);
std::unique_ptr<juce::InputStream> inputStream(juce::URLInputSource(url).createInputStream());
// Make sure the playback is stopped before loading a new file
StartStopMidiFile(false);
juce::MidiFile mf;
if (mf.readFrom(*inputStream.get()))
setMidiFileObject(mf);
}
void Unload()
{
StartStopMidiFile(false);
if (onEOF != nullptr) onEOF();
midifile.clear();
FileLoaded = false;
}
void Rewind()
{
tick_count = 0;
trackEventStart.fill(0);
}
void StartStopMidiFile(bool b)
{
// Call AllNotesOff when Stop button is hit
if (!b) allNotesOff();
// Don't play if no file is loaded
if (!FileLoaded)
{
if (onEOF != nullptr) onEOF();
return;
}
// No double play
if (b && PlayMidifile) return;
// Rewind
if (!b && !PlayMidifile)
{
sample_count = 0;
lastSampleOffset = 0.0;
Rewind();
}
// Store status
PlayMidifile = b;
}
void setTempo(double _tempoBPM)
{
tempoBPM = _tempoBPM;
// A single tick duration is 60 / BPM / TPQ
// Assuming tempo is 120 BPM and TPQ is 480, a tick lasts 1,0416666666666666666666666666667 milliseconds = 1041 microseconds
tick_duration = 60.0 / tempoBPM / (double)TPQ;
auto d = (lastSampleOffset + tick_duration * sampleRate);
tick_duration_in_samples = (int)d;
lastSampleOffset = d - (double)tick_duration_in_samples;
}
double getTempo()
{
return 60.0 / (double)TPQ / tick_duration;
}
float getPosition01()
{
return (float)tick_count / (float)last_tick;
}
void setPosition01(float pos)
{
allNotesOff();
tick_count = juce::jmap<float>(pos, 0, last_tick);
trackEventStart.fill(0);
}
void setSampleRate(double sr)
{
sampleRate = sr;
// Set a starting tick_length for a tempo of 120 BPM and 480 TPQ
//tick_duration = 60000.0 / 120.0 / 480.0;
//tick_duration_in_samples = tick_duration * sampleRate / 1000.f;
setTempo(tempoBPM);
sample_count = 0;
lastSampleOffset = 0.0;
Rewind();
}
// To be called in the audio loop for each single sample
void Play()
{
if (!PlayMidifile) return;
// Shift to the next tick after the required tick duration
if (++sample_count >= tick_duration_in_samples)
{
sample_count = 0;
// Play events from all tracks in the Midi File
for (int t = 0; t < midifile.getNumTracks(); t++)
{
// Get single track
auto Sequence = midifile.getTrack(t);
// Search events in track having a timestamp that matches the current tick_count
for (int e = trackEventStart[t]; e < Sequence->getNumEvents(); ++e)
{
auto msg = Sequence->getEventPointer(e)->message;
// Play all events matching this timestamp
if (msg.getTimeStamp() == (double)tick_count)
{
// Update tempo if it's a tempo event
if (msg.isTempoMetaEvent())
{
TPQ = midifile.getTimeFormat();
tick_duration = msg.getTempoMetaEventTickLength(TPQ);
setTempo(getTempo());
}
// Send Midi
processMidi(msg.getRawData(), msg.getRawDataSize());
}
// If the timestamp goes past the tick_count, skip scanning the rest of the track
if (msg.getTimeStamp() > (double)tick_count)
{
// Store last event index
trackEventStart.set(t, e);
break;
}
}
}
// Advance to the next tick
tick_count++;
// Check end of file and stop
if (tick_count >= last_tick)
{
PlayMidifile = false;
allNotesOff();
if (onEOF != nullptr) onEOF();
}
}
}
//==============================================================================
// These public variables tell whether the file is loaded and is playing
MidiFile midifile;
bool PlayMidifile = false;
bool FileLoaded = false;
int TPQ = 480;
// Set a lambda for receiving Midi Data
std::function<void(const unsigned char* midiData, int chunkLength)> processMidi;
// Set a lambda for calling the All-notes-off event on stop
std::function<void(void)> allNotesOff;
// Set a lambda that, if defined, is called when a new Midi File is loaded
std::function<void(void)> onLoad = nullptr;
// Set a lambda that, if defined, is called when the playback reaches the end of the file
std::function<void(void)> onEOF = nullptr;
private:
juce::Array<int> trackEventStart;
double tick_duration = 1.041; // 120 BPM @ 480 TPQ
long tick_count = 0, last_tick = 1;
int sample_count = 0, tick_duration_in_samples = 0;
double sampleRate = 44100.0, lastSampleOffset = 0.0;
double tempoBPM = 120.0;
};
Call the Play() function for each sample in the processBlock() function.
Basically, in your constructor you create the object and define the callbacks:
Constructor()
{
// You have declared a class member:
// std::unique_ptr<JuceMidiFilePlayer> player
player.reset(new JuceMidiFilePlayer());
// Define the callbacks
player->processMidi = [&](const unsigned char* midiData, int chunkLength)
{
// This or any other way to send Midi Data to your synth
mySynth->processMidi(midiData, chunkLength);
};
player->allNotesOff = [&] { mySynth->allNotesOff(); };
player->onLoad = [&]
{
// Things to do as soon as a new Midifile has been loaded...
};
player->onEOF = [&]
{
// Things to do when the player reaches the end of the Midifile...
};
}
// Where you know the sampleRate...
void prepareToPlay(double sampleRate, int samplesPerBlock)
{
player->setSampleRate(sampleRate);
}
// Where you load the Midifile...
void loadMidiFile()
{
// create file chooser and get the file...
// ...
player->LoadMidiFileFromURL(myFile);
}
// In the audio loop
void processBlock(AudioBuffer<float>& buffer, MidiBuffer& midiMessages)
{
// For each sample...
for (int s = 0; s < buffer.getNumOfSamples(); s++)
{
player->Play();
}
}