Determining Whether Blocks are Consecutive in Time

I have some functionality in my plugin that does some weighted moving averaging over multiple frames. It needs to reset when the playhead is moved, i.e. the frames aren’t consecutive in time. My current implementation trys to getTimeInSamples from the playhead position and calculate where it should be based on the previous position and the frame size. If the DAW doesn’t provide that information, it falls back to checking if play/stop has been toggled. It’s made very clunky by the different ways the different hosts handle this information. Pyramix, for example, provides the optional timeInSamples parameter, but it’s suck at 0 all the time. Wavelab stops calling processBlock when the transport is stopped, but others continue to do so. Wavelab also reports the blockSize with a factor applied to it. Etc., Etc.. Below is my current implementation. Any ideas for a smarter approach?

            /* We want to reset delayEstimator whenever the playhead is moved in the DAW, becuase if I am listening to
             * one part of a song in the DAW, and then I move to another part of it, I don't want the delay estimator
             * to use the previous estimations in determining the average delay that it reports. There are a couple of
             * ways to determine if the playhead has been moved. The preferred method is to read out the current
             * position of the playhead in samples and, if the DAW transport is in play mode, see if it is exactly
             * one frame ahead of the location from the last location - if so, these are consecutive frames, and we
             * don't need to perform a reset - if not, the playhead has been moved, and we do want to reset.
             * With some DAW/plugin format combinations we can't get the current time in samples though (Pyramix).
             * In those cases, we check to see if the DAW transport is in play mode or stop mode and if that changes
             * we reset. This is less robust for a couple of reasons. 1: In cases where the DAW stops calling
             * processBlock when the transport is stopped (like WaveLab) you'd never actually see stop mode so it would
             * never tigger a reset, and 2: If you stop playback and then restart from the same spot, it will trigger a
             * reset when it actually shouldn't. */

            // playHead has transport info provided by DAW
            const auto position = getPlayHead()->getPosition(); 
            const auto isPlaying = position->getIsPlaying();

            // If this is the first block, we need to store some values for comparison, and we don't need to evaluate
            // whether to reset delayEstimator
            if (getFirstBlockFlag() && isPlaying) 
            {
                setFirstBlockFlag (false);
                setWasPlaying (true);
                if (position->getTimeInSamples()) // If DAW provides this info, grab it
                {
                    setPrevTimeInSamples (*position->getTimeInSamples());
                }
            }
            else // Not the first block played
            {
                if (isPlaying)
                {
                    /* If the DAW provides this info, we want to use it. In some cases (Pyramix), though, the DAW does
                     * provide the optional timeInSamples parameter, but it is stuck at 0. In that case, we want to fall
                     * back to the getWasPlaying method. This implementation means a true timeInSamples = 0 condition
                     * won't trigger a reset, so there will be cases where the delayEstimator doesn't reset until the
                     * second block in when the playhead is moved back to the beginning, but it feels more robust than
                     * just checking what host we are in and having behavior tied to each DAW. */
                    if (position->getTimeInSamples() && position->getTimeInSamples() != 0)
                    {
                        const auto currentTimeInSamples = *position->getTimeInSamples();
                        if (currentTimeInSamples != getPrevTimeInSamples() + prevFrameLen)
                        {
                            delayEstimator.reset(); // This frame is not consecutive with the previous one
                        }
                        setPrevTimeInSamples (currentTimeInSamples);
                    }
                    else if (!getWasPlaying()) // Can't get currentTimeInSamples - use Play/Stop method
                    {
                        delayEstimator.reset(); // Play/Stop has been toggled
                    }
                }
                setWasPlaying (isPlaying);
            }```

have you tried
*getPlayHead()->getPosition()->getPpqPosition())
?
that should work in any running DAW

1 Like

Make sure you’re clearing state in reset(), which should be called when the host seeks. Whether or not the host stops rendering when transport stops depends on the other plugins in the chain, so you can’t rely on that.

Timing information in the processBlock callback is super janky and unreliable across hosts, making your DSP robust enough not to care about this is easier.

2 Likes

yes, reset() is the way, I reset delays and stuff on reset() too

1 Like

Had no idea - this seems way simpler. I’ll give it a try - thanks.

1 Like

Looks like Wavelab doesn’t call reset(). I’ll add it in but it looks like i’ll need to keep the other methods also.

how are you testing this and what problems are you seeing?

Just debugging my VST3 into Waveleb 12 elements on mac. A breakpoint in my overridden Reset() never triggers no matter what I do with the transport. Seems like it’s maybe a thing: Processor.reset() is not called - Example travel & time continuity check here

ah, ok haha, that is my own code and thread there! ok, I kind of forgot about that LOL.. :wink:
So I had the exact same issues with it, and I make some continuity check code, that works.
But now to that issue with *position->getTimeInSamples();
is that still relevant? I see I use it too, would be good to know that this is not an issue.

getTimeInSamples definitely does not seem to work in Pyramix (I think that’s the only place I’ve seen it fail so far). What’s even more frustrating is that it does provide the optional parameter the value is just always 0.

1 Like

ok, now if only Pyramix has this bug, it is no big problem, for me at least. Never heard of the app. I just check on Reaper, Logic, Cubase, Studio One etc.
In my code a timeinsamples of 0 will assume a pause, and it will not reset:
my code treats pauses as “let reverberate”.

The time continuity check in my code, is for cutting the tail when a track is disabled in a live monitoring situation: after returning to the track with your plugin, the tail from way before is still in the buffer. So this will detect if the plugin was disabled in the meantime. Bypassing the plugin will have the same effect: at power up, reset() will be called, because of the time gap.

Basically this inside processBlock:

int64_t sampleOffset = -1;

// detect playback state and get sample offset (if available)
AudioPlayHead* playhead = getPlayHead();
if (playhead) {
    if (auto positionInfo = playhead->getPosition()) {
        if (auto timeInSamples = positionInfo->getTimeInSamples()) {
            timeline = positionInfo->getIsPlaying();
            if (timeline)
                sampleOffset = *timeInSamples;
        }
    }
}

// detect discontinuity
if (m_sampleOffsetExpected != sampleOffset) {
    // do whatever reset you need to do
    reset();
}

// update expected next sample offset
m_sampleOffsetExpected = (sampleOffset != -1) ? (sampleOffset + blockSize) : -1;