Hello,
I’m building some simple MIDI-based sequencing examples (without audio). For timing the sync of the sequencer obviously the Timer class is not suitable (unless I misunderstand something). Instead I’m using a Thread which seems pretty stable using a constant sleep() value in a loop in its run() method. But I can imagine that should the loop get even slightly more complex the timing might drift slightly. I figured that I could time how long each loop’s iteration takes to run and subtract this from the constant value sent to sleep to compensate. I’m also compensating for the int/double rounding errors on subsequent loop iterations, and the duration of the iteration-time measurement functions themselves (by doing an average at the start of the app). Does this seem a sensible approach?
#ifndef _MAINCOMPONENT_H_
#define _MAINCOMPONENT_H_
#include <juce/juce.h>
class MainComponent : public Component,
public ButtonListener,
public ComboBoxListener,
public Thread
{
private:
StringArray midiDevices;
MidiOutput* midiOutput;
ComboBox* midiOutputSelector;
TextButton* play;
TextButton* stop;
Label* text;
Array<int> noteSeq;
int index;
int rate;
double timeCorrectionError;
double measurementError;
public:
//==============================================================================
juce_UseDebuggingNewOperator
MainComponent ()
: Thread(T("Sequencer")),
midiOutput(0),
index(0),
rate(125),
timeCorrectionError(0.0),
measurementError(0.0)
{
// simple sequencing example, using a thread
midiDevices = MidiOutput::getDevices();
addAndMakeVisible(midiOutputSelector = new ComboBox("MIDI Output Selector"));
midiOutputSelector->setBounds(10, 10, 270, 20);
midiOutputSelector->addListener(this);
for(int i = 0; i < midiDevices.size(); i++)
{
midiOutputSelector->addItem(midiDevices[i], i+1);
}
midiOutputSelector->setSelectedId(1, false);
addAndMakeVisible(play = new TextButton(T("Play")));
play->setBounds(10, 40, 270, 20);
play->addButtonListener(this);
addAndMakeVisible(stop = new TextButton(T("Stop")));
stop->setBounds(10, 70, 270, 20);
stop->addButtonListener(this);
addAndMakeVisible(text = new Label(T("Text"),T("Starting...")));
text->setBounds(10, 100, 270, 20);
// add some notes to the sequence
noteSeq.add(60);
noteSeq.add(62);
noteSeq.add(64);
noteSeq.add(65);
noteSeq.add(67);
noteSeq.add(65);
noteSeq.add(64);
noteSeq.add(62);
// measure the measurement error!
int n = 100000; // iterations for the test
double oneOverN = 1.0/n; // for calulcating the mean
double meanError = 0.0; // accumulate the mean error
for(int i = 0; i < n; i++)
{
double time1 = Time::getMillisecondCounterHiRes(); // start time
Time::getMillisecondCounterHiRes(); // dummy for the loop top meaurement
Time::getMillisecondCounterHiRes(); // dummy for the loop bottom meaurement
getTimeCorrection(0.0, 0.0); // dummy for the calculations
double time4 = Time::getMillisecondCounterHiRes(); // end time
meanError += (time4-time1) * oneOverN;
}
measurementError = meanError;
text->setText(String(T("Measurement error: "))+String(measurementError, 6)+String(T("ms")), false);
}
~MainComponent ()
{
deleteAllChildren();
}
//==============================================================================
void buttonClicked(Button* button)
{
if(button == play)
{
text->setText(T("Playing"), false);
startThread(10); // 10 = highest priority
}
else if(button == stop)
{
text->setText(T("Stopped"), false);
stopThread(3000); // stop and timeout after 3 secs and then force if necessary
}
}
void comboBoxChanged(ComboBox* comboBox)
{
if(comboBox == midiOutputSelector)
{
midiOutput = MidiOutput::openDevice(midiOutputSelector->getSelectedItemIndex());
}
}
forcedinline int getTimeCorrection(double loopTopTime, double loopBottomTime)
{
double timeCorrection = loopBottomTime-loopTopTime+measurementError; // time the main body of the loop took to run (plus these measurements)
double timeCorrectionWithPrevErr = timeCorrection + timeCorrectionError; // add on the error from the last iteration
int timeCorrectionRounded = roundDoubleToInt(timeCorrectionWithPrevErr); // round this to an int since sleep() needs int ms
timeCorrectionError = timeCorrectionWithPrevErr-timeCorrectionRounded; // measure the error between int & double for next time
return timeCorrectionRounded;
}
void run()
{
while( ! threadShouldExit() )
{
double loopTopTime = Time::getMillisecondCounterHiRes();
if(midiOutput != 0)
{
midiOutput->sendMessageNow(MidiMessage::noteOff(1, noteSeq[(index-1) % noteSeq.size()]));
midiOutput->sendMessageNow(MidiMessage::noteOn(1, noteSeq[index % noteSeq.size()], 0.9f));
}
index++;
double loopBottomTime = Time::getMillisecondCounterHiRes();
sleep(jmax(0, rate - getTimeCorrection(loopTopTime, loopBottomTime)));
}
if(midiOutput != 0)
midiOutput->sendMessageNow(MidiMessage::allNotesOff(1));
}
};
#endif//_MAINCOMPONENT_H_