MIDI sequence timer

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_ 

It sounds workable, if a little convoluted. I do something like this:

getTimeNow (do this first)
do what I need to do
getTimeNow
work out how much time elapsed
sleep for period - elapsed

As with anything multi-threaded, this could get pre-empted which would throw timing off, but other solutions suffer from the same problem.

BTW, I’m doing sleep on Mac and Linux with nanosleep, as it’s more accurate that Thread::sleep, there’s probably a Windows equivalent.

Bruce

I wrote Time::waitForMillisecondCounter to do exactly this - that’s what tracktion uses in its midi playback thread, and it works pretty well.

Unless you’re using a very old juce version, I think Thread::sleep() just calls nanosleep().

But in whole millisecs? That’s a bit coarser than I prefer. Can we call that a feature request then please, a wait method with microseconds or nanoseconds?

Bruce