PPQ Position and Actual transport start point

Here is the logic/calculation I am doing…

    // === Loop Wrap and Note-On Detection ===
    if (posInfo.isLooping && !lastNoteWasNoteOn)
    {
        const double loopLengthPPQ = loopEndPPQ - loopStartPPQ;

        //set the previousPPQ to the start of the loop
        double previousPPQ = ppqStart;

        //iterate through the samples in the buffer
        for (int sample = 0; sample < numSamples; ++sample)
        {
            // Calculate the current PPQ position based on the sample time
            const double currentPPQ = ppqStart + (sample * ppqPerSample);

            // Calculate the wrapped PPQ positions
            const double previousWrappedPPQ = fmod(previousPPQ - loopStartPPQ + loopLengthPPQ, loopLengthPPQ);
            const double currentWrappedPPQ = fmod(currentPPQ - loopStartPPQ + loopLengthPPQ, loopLengthPPQ);

            // Check if the current PPQ is less than the previous PPQ - we have wrapped
            const bool wrapped = currentWrappedPPQ < previousWrappedPPQ;

            if (wrapped && !lastNoteWasNoteOn)
            {
                const int sampleOffset = juce::jlimit(0, numSamples - 1, sample);
                midiMessages.addEvent(juce::MidiMessage::noteOn(1, 60, (juce::uint8)127), sampleOffset);
                lastNoteWasNoteOn = true;
                samplesFromLastNoteOnUntilBufferEnds = numSamples - sampleOffset;

                break; // Only trigger once per buffer
            }

            //update the previous PPQ for the next iteration
            previousPPQ = currentPPQ;
        }
    }

And the result is that sometimes the note is placed perfectly at the loop start after looping but other times the note is placed precisely at the end of the bar after looping - actually outside of the loop! This is what is most baffling. If it was scheduled slightly late after the loop point then I would expect the note to be slightly late and a little bit more than 0.0 - but to get placed in the right take but all the way to the end and outside of the loop points - that is baffling for me. There is no product specific info here @eyalamir right?

To write this code and make sure your math is correct, you need to write some unit tests, or at the minimum a console app sending concrete position values and see exactly what your logic does in terms of sample position, etc.

Also: I’m not sure it’s correct to only do this logic once per buffer. I think there could be multiple loop points (because you’re also looping, and the host is looping), so I think you need to run the whole thing every sample.

But I really didn’t look too much into this - this is a heavyweight product problem, like the one you usually hire me to solve over a few days with proper unit tests and a reliable way to check that my math/logic is correct.

Also it’s important to note that I don’t like that idea of storing some “previous ppq”. Instead, when I create a sequencer/arpeggiator/etc, I set a ‘range’ of PPQ, and then for every PPQ given at a particular sample, I can decide if it’s time to start or stop a note.

This is useful because the time given by the DAW may ‘jump’ a bit and not be 100% consistent, and I want to make sure that I handle host buffers in a way that is fully matching my logic and not based on the previous buffers that might not match it.

I’m not storing any previous ppq position across buffers, this variable is only stored within the loop that iterates through the current buffer. But I guess you didn’t look too closely because of this being a problem that people must pay you to solve.

It’s not about money, it’s just that usually when I solve this problem I have to spend a few days creating a proper way to represent the model, test it externally from any host, making sure that I can handle all the edge cases of the DAW throwing random PPQ positions at me, no stuck notes… etc.

I can step through and debug your math until I find this bug you’re experiencing, but without a proper model and a way to test this it would just delay the problem again into the next bug. It’s very hard to engineer a full solution through a forum post.

1 Like

I also think this is something to be unit tested. It’s text book why there are unit tests. I’d recommend writing a few basic tests first, and then in addition writing tests with real PPQ/buffersize values being supplied by what ever DAW you want to use.
Apperently it works sometimes and sometimes it doesn’t? That is very odd behaviour for these kinds of algorithms but also a great place to start out at unit tests. FYI: first thing I’d suspect is a rounding error.

If you are set on solving it “in your head” so to speak or with help here in this forum, I suggest you try to work on communications of the problem. For me now reading it, it’s very hard to grasp A) what you are trying to accomplish and B) what exactly doesn’t work. Post a few screenshots of what is supposed to happen vs. what actually happens. Draw a sketch etc. This is not only helping us, but also helping you to sort your thoughts. Maybe it’s gonna come to you just by writing about it.
eyalamir already gave you very valuable feedback, that the PPQ position is theoretically correctly transmitted from the DAW, so you at least know that what you want to do is possible with the correct math.

Last tip concerning this specific problem: there are multiple layers of complexity to this problem, that make it hard to grasp (as in doing the math in your head). That is very evident by the numbers in your debug output.
As a starting point, I’d suggest assuming that PPQN and tempo is defined in a way, that one PPQ is exactly one sample. As another step, assume the buffer size to be 10 samples (probably avoid 1, that might be an odd edge-case).

1 Like

Yes, I think you’re both right that this kind of thing does call for unit tests and probably I haven’t been good at communicating the challenge. I will definitely try to write some tests and now try to explain the problem I am attempting to solve.

What I want is to trigger a midi note-on at the exact start point after a loop has happened. So if the loop start PPQ is 0.0 and the loop end PPQ is 4.0 - I want a note to be placed at 0.0.

That’s the challenge. :slightly_smiling_face:

Now this is how I have approached the problem…

We are given the start position of the current block and we are given the number of samples in the buffer. So my thinking is that we need to figure out if we will reach or cross the loop point in the current block and at which sample we will make that transition.

So the first thing is to work out if we loop in the current buffer.

We start at posInfo.ppqPosition
I calculate where we will end by taking the posInfo.ppqPosition and adding the number of PPQ’s per sample (bpm / 60.0) / sampleRate multiplied by the number of samples in the buffer buffer.getNumSamples() -1. I substract one from the number of samples because at the first sample we are at posInfo.ppqPosition.

Next, I check if our calculated end PPQ is greater than the loopEndPPQ - if it is then at some point in this buffer we will cross the loop boundary.

If we cross the loop boundary we need to know at which sample that will happen so that we can use that sample as the offset for the midi-note.

To do this I iterate through the samples in the buffer and for every sample calculate the PPQ position. Because we are looping between 0.0 and 4.0 we need to wrap our positions so that any position calculated above 4.0 wraps around to zero.

I compare the (wrapped) PPQ values of the current sample with the previous and when it is less than the previous I know we have transitioned across the loop - past 4.0 to 0.0 or more.

This sample is then the value I should use for the offset of the midi-note.

My results using this logic are inconsistent.

Sometimes it works and the note is places at 0.0 exaclty - zero samples. But other times I see this…

After the loop just happened when the transport is just into take 2 there is no note at the start.

Even more strangely, considering the transport is quite far from the end of take 2. When I stop and open the midi editor of take 2 I see this…

There is a note exactly at the end of the loop! Before the transport has even reached that point! I thought that maybe it was on the end of the previous take and just shows up because take 2 is on top of take 1 which would make more sense but when I drag take 2 down on to it’s own track it is still part of take 2 and not take 1. :sweat_smile:

Anyway, I hope this gives some better understanding of what I am trying to do and what I am seeing happen @Rincewind.

I’m sorry @eyalamir if I am asking about stuff that is considered to be beyond the realms of forum help - I understand if it is the kind of thing that people are expected to look at in a professional context. I’m not a pro by any means - just a hobbyist that is trying to do something that I thought would be quite commonplace in terms of plugin development and hoped that what I was asking was a standard problem that many have faced and solved. Honestly, I would be happy to pay someone to help as your knowledge and time is invaluable and I very much respect that!

So thank you both for your advice and I will try to write some tests to see if it helps.

So far I have debugged and logged the reported PPQ position from Cubase so that I can compare them to my expected positions and they match exactly. Occasionally there is a difference of 0.000001 PPQ - never more than though so the calculated position in the buffer seem pretty accurate.

I would suggest to divide this problem to a few different, smaller problems.

  1. Make sure that for each range of PPQs (start/end) that does NOT contain any looping, you can generate the correct note on and note offs.

The way I do this is with a range - so a if a quarter note is to be played between 1-2 PPQ, any PPQ value outside of that range would check if that note is already played and stop it. Any value inside of that range would check if it isn’t played, and play it.
(I’m simplifying, but that’s the gist of it).

  1. Test this in a console app or unit test and NOT in a DAW, to make sure you can handle any PPQ values in any order, including ranges like 3-4.3, and suddenly 2.1-3.5 right after and still produce correct results.

  2. Make sure that externally to the function you wrote in #1, you have a looping mechanism that validates both your looping and the host looping, and passes a series of non-looped transport into the function called in #1.

So for example the host calls you with 1-4, but a loop point at 3. You added another, internal loop at 2… Handle that and create a normalized transport that your internal function understands that only has ‘forward’ ranges in it.

You can also test this function separately from a DAW.

Ok, I tried doing as you suggested - in fact I was already breaking it down into smaller problems which is why I was focusing on one thing exactly- placing the first note after looping. Nothing more than that part.

I already had solved normal playback without looping and have been able to trigger notes on/off with really great sample accuracy. The only thing that I was baffled by with that was the Cubase pre-roll, anticipative midi recording behaviour which you pointed out is not an issue.

I used your suggestion of a range and yes it works great. Normal playback is solid and it’s a neat way to tackle the job.

Next I did as you said and ‘normailzed’ the ppq so that I could pass them to the same function that handles forward playback - splitting the ppq into ranges that are forwards only.

However I run into the same issues again. At the loop end the note is triggered but for some reason Cubase places this note at the very end of the take - outside of the loop and before the transport has reached that point. As I posted in the screen shot.

Both my old logic and the new logic work perfectly in Reaper but in Cubase it’s a different story.

I could absolutely write unti tests for the logic but I am sure that they will pass but still won’t shine any light on why it doesn’t work in Cubase but does in Reaper.

I would be really grateful if you would take a look and yes, I would be happy to pay you for your time.

It appears that your example is not available on GitHub.
Would you be willing to share it again?

Ok, so I’ve made some progress on this and figured out that my solution does in fact work in Cubase perfectly - but it depends on what midi recording mode is selected. In cycle overwrite mode the note is placed correctly every single time and never fails - in cycle stacked mode is where it is not reliable! Strangely, when I log every buffer from Cubase and compare what it sends in overwrite vs stacked mode there is no difference in the reported values! Position Info PPQ, seconds and time in samples are pretty much the same so I’m not sure if this is something that I will be able to fix - but I am going to try. Now I have written a console app to test the engine, feeding the exact buffer info that I get from my Cubase logs and it appears to work in my tests - later I will try to use the same logic in the plugin and see how that goes - fingers crossed!

@appjuce - sure, once I have this working I will put it in a repo :slight_smile:

@eyalamir, I am really curious now to know if any of your plug-ins act a bit weird on the first note when recording the midi output when Cubase is in cycle record ‘stacked’ mode?

It seems like my plug-in is not the only one - I tried the same thing with Loomer Architect VST which has amazing sequencers with very precise timing and it behaved exactly the same!

So I really am leaning towards a Cubase quirk here because…

  1. It works perfectly when Cubase is in midi record cycle overwrite mode.
  2. It works perfectly in Reaper in any record mode.
  3. Loomer Architect exhibits the same issue

Of course, it could be some setting in Cubase that I haven’t found yet that makes this happen only on my machine but I would like to know if your plug-ins also behave like this or not. If not let me know which one so I can test it on my PC :slight_smile:

Yes, I also see that issue where it sometimes doesn’t record the first note in a loop, I think it’s just a Cubase bug with that mode. I don’t think it’s something you need to change in your plugin for that to work.

2 Likes

Thanks for the confirmation!

It’s great to know it’s not something I can do much about - I have used many hours trying to get that to work consistently and despite the numbers in my unit tests being good - even when feeding the exact same number that Cubase reports on every buffer - when it comes to how it behaves in Cubase it just is not reliable.

Thanks again! :slight_smile:

1 Like

If you get to the point where there’s some code in a repo, it would be interesting…