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!