Inconsistent arpeggiations

Hello jucers,

I could really use some wisdom on why the arpeggiator is sometimes perfectly in sync with the beat of the host (Cubase) and other times perfectly ‘off-beat’.

I have taken the basic arpeggiator from the tutorials and attempted to make it sync with the DAW.

So instead of the slider dictating the rate - I have gotten the host bpm, sample rate and buffer size and calculated which samples the notes need to fall on to hit 16th notes.

The code looks like this…

class Arpeggiator  : public AudioProcessor
{
public:
    Arpeggiator()
        : AudioProcessor (BusesProperties()
			.withInput("Input", AudioChannelSet::stereo(), true)
		) 
    {
    }

    ~Arpeggiator() {}

    void prepareToPlay (double sampleRate, int) override
    {
        notes.clear();
        currentNote = 0;
        lastNoteValue = -1;
        time = 0.0;
        rate = static_cast<float> (sampleRate); // get the sample rate
    }

    void releaseResources() override {}

    void processBlock (AudioBuffer<float>& buffer, MidiBuffer& midiMessages) override
    {
		AudioPlayHead* playHead = getPlayHead();
		if (playHead == nullptr) return;

		AudioPlayHead::CurrentPositionInfo positionInfo;
		playHead->getCurrentPosition(positionInfo);  // get the current position from the playhead

		auto bpm = positionInfo.bpm;				//bpm is quarterNotesPerMinute
		auto bps = bpm / 60;						//bps is quarterNotesPerSecond
		auto samplesPerBeat = rate / bps;			//number of samples per beat/quarternote is samples per sec / beats per second

        // set note duration
		auto noteDuration = static_cast<int> (std::ceil(samplesPerBeat * 0.25f ));		// for 16th note divisions

		auto numSamples = buffer.getNumSamples();	// number of samples in each buffer slice


        MidiMessage message;
        int ignore;

        for (MidiBuffer::Iterator it (midiMessages); it.getNextEvent (message, ignore);)
        {
			if (message.isNoteOn())
			{
				notes.add(message.getNoteNumber());
			}
			else if (message.isNoteOff())
			{
				notes.removeValue(message.getNoteNumber());
			}
        }

        midiMessages.clear();

        if ((time + numSamples) >= noteDuration)
        {
            auto offset = jmax (0, jmin ((int) (noteDuration - time), numSamples - 1)); 

            if (lastNoteValue > 0) // if last note was a note-on
            {
                midiMessages.addEvent (MidiMessage::noteOff (1, lastNoteValue), offset);
                lastNoteValue = -1;  // set flag to indicate last note was a note-off
            }

            if (notes.size() > 0) // if there are notes in 'notes' coolection
            {
                currentNote = (currentNote + 1) % notes.size();  // advance to next note in collection
                lastNoteValue = notes[currentNote];  // set last note flag to indicate a note-on
                midiMessages.addEvent (MidiMessage::noteOn  (1, lastNoteValue, (uint8) 127), offset); // add last note to buffer at sample pos = offset
            }

        }

        time = (time + numSamples) % noteDuration; // update time
    }

And when it works it works great - but sometimes it starts slightly off beat and then it stays like that unless I stop/start the transport, or adjust the tempo, or adjust the playback position. This may or may not cause the arpeggiator to start on beat again.

This seems to indicate that the problem has to do with the initial position information that is set from the playhead because once the arp is triggered the first time it will stay good/bad so long as the transport is not interrupted. re-triggering the arp will have no effect at all.

Please can anybody shine some light on what might be happening or how it could be fixed?

Any ideas much appreciated :slight_smile:

Don’t you need to use ppqPosition and ppqPositionAtLastBarStart from CurrentPositionInfo to make this work in time?

maybe! I mean it stays in time using the code above but the first time the arp is triggered will determine if the notes fall on the beat or slightly off. How might I use ppqPosition and ppqPositionAtLastBarStart to make things work?

if ((time + numSamples) >= noteDuration)

small bug possibly: In the case where noteDuration = numSamples, and time = 0, then the next MIDI event should occur at the start of the next processBlock (currently you are sending it in the current processBlock). i.e. the correct code, I believe should be:

if ((time + numSamples) > noteDuration)

The cause of your main bug though appears to be that variable ‘time’ is synced only to the very start of audio streaming, which is not necessarily on a bar start (some DAWs start the music transport with some ‘pre-roll’ a little before bar number 1). You need to read the DAW ‘song position’ and sync ‘time’ to that.

1 Like

Many thanks for the input!

Yes, you’re correct - for the case where the note duration is exactly equal to the numSamples I wasn’t sure how to get the note to start in the next block so I went with the compromise of having it 1 sample early at the very end of the current block (numSamples - 1) - It seemed like the processing must occur within the current block, but now that you mention it, perhaps I could have a flag that gets set so I can know if this situation occurs and can send a note on.off at the start of the next block.

Yes, this sounds like the culprit. I’ll have to think hard how I can get ‘time’ to start synced to the next quarter note position of the DAW. Thank you so much for the help :smiley:

1 Like

I find working with samples and ppqPosition (which is a stupid variable name, just means quarter note position), a proper head scratcher. You usually just have to multiply something by something else, and then add something or whatever, it’s not hard math, but it feels like it’s easier to do filter theory …

You’ll also eventually wonder: what happens when the DAW loops…

3 Likes

Headscratcher indeed - i’ve been pulling my hair out! I don’t even wan’t to contemplate how to handle looping or time sigs yet and i’m already in despair :stuck_out_tongue_winking_eye:

Tonight I have a little time to spend on this problem and i’m guessing I will need to use some of these ‘ppqposition’ units. Am I correct in thinking that the first quarter-note happens at PPQP = 0.0, the 2nd at 1.0, 3rd at 2.0 and 4th at 3.0? then 16th notes would be 0.0 -> 0.25 -> 0.5 -> 0.75…?

It might start at position 1.0, I think. I dont’ think you can rely on hosts being consistent either. In one old version of logic the ppqPosition seemed to drift backwards from time to time - though I’ve not seen that terrible behaviour recently.

This was a very accurate prediction! I’m scratching my head now because when the Reaper loops buffer.getNumSamples() and positionInfo.ppqPosition are not what I expected. At the start of every beat, except the last beat when it will loop, the number of samples in the buffer is consistent and is say 1024 samples. When I call buffer.getNumSamples() at the start of the block for the last beat in the loop and also for the 1st beat after looping then the number of samples will vary. Adding the number of samples for the last beat and the first beat then it always sums to 1024 but the values for each will be different each time. It seems like something behind the scenes knows about the position in the loop and so adjusts the buffer size for the last and first beat. Additionally - and as a result of this buffer calculation, the positionInfo.ppqPosition is always exactly 0 at the start of the block after a loop point. The result is that it’s causing my note-off offsets to be wrong - ughh :stuck_out_tongue:

1 Like

well that behaviour sounds sane at least. i’d check with some other daws too!

1 Like

good grief that took me longer than it should - spent hours debugging before I found the fix. My first note-off after each loop was being calculated wrong. I just needed to move the line of code that updated the number of samples since the last note-on to the start of the processBlock instead of at the very end lol!

1 Like