MIDI timing notes / arpeggiator


#1

I’ve written a simple example arpeggiator. This varies from the arpeggiator demo example as I’m trying to time notes accurately based on the hosts BPM.

I’m having a problem with timing however. In theory the way I’ve done it should work, but listening back there is slight timing drift on some notes. This is perhaps apparent when the BPM is at 130 rather than e.g. 120 (everything divides equally at 120 bpm, that may be something).

Anyway, it’s been cracking me up a while now. Below is a simple example that effectively counts samples from the audioBuffer and uses that for timing inline with calculating the samplesPerStep based on the BPM and sample rate (48000 in my DAW). I’ve seen this in examples and it seems to be a good approach (though I’m open to any other suggestions). There is obviously a bit more I can do here when the sequencer is playing to sync things up using transport info, but I’ve kept this example simple. Odd steps send a note on, even a note off, and one long note triggers this going.

Code (_underscore variables indicate member variables):

void ArpSimpleTestAudioProcessor::processBlock (AudioBuffer<float>& buffer, MidiBuffer& midiMessages)
{
	AudioPlayHead* playHead = getPlayHead();
	if (playHead == nullptr) return; // Need a playhead

	AudioPlayHead::CurrentPositionInfo positionInfo;
	playHead->getCurrentPosition(positionInfo);

	MidiMessage midiMessage;
	int samplePos = 0;

	MidiBuffer keepMessages;

	// Look for note ons and set held note, unhold with a note off if not playing
	for (MidiBuffer::Iterator it(midiMessages); it.getNextEvent(midiMessage, samplePos);)
	{
		if (midiMessage.isNoteOn()) _heldNote = midiMessage.getNoteNumber();

		if (midiMessage.isNoteOff()) {
			_heldNote = -1;

			if (!positionInfo.isPlaying)
				keepMessages.addEvent(midiMessage, samplePos); // Let the note off go
		}
	}

	keepMessages.swapWith(midiMessages); // MIDI messages now contain everything but note ons (we're generating them)

	if (_heldNote > -1)
	{
		_sampleCounter = _sampleCounter + buffer.getNumSamples(); // Increment our counter
	}
	else if (!positionInfo.isPlaying) // If the sequencer isn't playing, reset
	{
		_sampleCounter = 0;
		_nextStepInSamples = 0;
		return;
	}

	auto bpm = positionInfo.bpm;
	auto bps = (bpm / 60);
	auto samplesPerStep = (_sampleRate / bps) / 4; // At 120bpm / 48000, this is 6000 samples per step

	// Time to play
	if (_sampleCounter >= _nextStepInSamples && _heldNote > -1)
	{
		int pos = 0; 
		
		if (_nextStepInSamples > 0) pos = _sampleCounter - _nextStepInSamples -1;
		if (pos == -1) pos = 0;

		if (_wasNoteOn) // A simple bool for the note on / note off toggle
		{
			midiMessages.addEvent(MidiMessage::noteOff(1, _heldNote, 0.0f), pos); // Note off
			_midiLog.push_back("Note off: " + String(_sampleCounter) + "+" + String(pos) + "=" + String(_sampleCounter + pos));
		}
		else
		{
			midiMessages.addEvent(MidiMessage::noteOn(1, _heldNote, 1.0f), pos); // Note on
			_midiLog.push_back("Note on: " + String(_sampleCounter) + "+" + String(pos) + "=" + String(_sampleCounter + pos));
		}		

		_wasNoteOn = !_wasNoteOn; // Toggle note on / note off

		_nextStepInSamples += samplesPerStep; // Set our next step
	}
}

My main questions are: is this approach sound? Why might I be getting this timing drift?

I can attach an example if that makes things easier.

Any help appreciated!


#2

without reading your original message too deeply, have you seen this project in juce examples?


#3

Yes I mention it on the first line of my post! It does its own timing rather than use the DAWs BPM so unfortunately isn’t much help here.


#4

My advice would be to start with the arpeggiator demo. When you’ve got that working, just modify it to calculate it’s speed parameter from the playhead bpm info instead of reading a slider.


#5

I did try that also without success. I guess I preferred rewriting in my own words and also wanted to change some behaviours.

Anyhoo I’ve been working on it this morning and think I’ve got is solved by resetting the sampleCounter after each step goes out rather than keeping the count going (I can only guess it was probably hitting big numbers quickly / a precision issue).