Why use a timer in an audio application?
The sampling rate is your timer… you already have something that ticks on the clock, the audio clock.
Don’t count milliseconds, count samples instead.
At a 44100 Hz samplerate, 441 samples are 10 milliseconds… You can compute the proportion between Midi tempo, time signature, tempo division and sampling rate, and establish how many samples you should wait between a note event and the next one. Wherever you have fractional samples, you can round to the nearest integer and bring back the fractional part to the next interval.
Check this code, it’s part of a MidiFile player that I wrote… The Play() function must be called at every sample in the audio loop. See how the timing is calculated in setSampleRate() and setTempo()
#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;
};
