Best practice on plugins which require AudioPlayhead information and Transport management

i was wondering if anyone could provide me some insight on the best way of handling transport controls and AudioPlayhead related stuff in a plugin?

a juce plugin thats run in standalone has no real notion of a timeline, bpm or any timecode information (which is fair enough as the standalone filter is only using a simple AudioProcessorPlayer to get things moving), so from a code/design perspective and as plugin developers, should we be responsible for managing a Transport type class ourselves?

i’m trying to think of the best way to handle a plugin which uses a host’s playhead position info for various reasons (things like to update a sequencer position, its own internal timeline editor, play an arpeggiator or a tempo synced delay etc) - but could also handle cases where it uses its own position info if needed, i guess something like a sync to host or un-sync option.

are there any tutorials, tips or examples out there anyone would be happy to share?

this topic has various opinions, keep that in mind!
here’s mine:

the fact that almost every single bit of information in the playhead/transport is optional is both technically accurate and practically irrelevant.

you might for example run a plugin in cantabile, which has no ppq and bpm. what would you want your temposync features to do then? you might want them to never even pop up in the first place. but you can’t do that because the transport info is not yet available in the constructor and prepareToPlay, but only in processBlock. so the features always need to be there, in case the DAW suddenly changes its mind about supporting it in the middle of the audio process. for a situation like that it might still be ok to treat the playhead as optional in processBlock and make its related components invisible in the editor.

But what if, like you said, you just wanna run the plugin in standalone to debug certain features? What if those features are related to transport information? suddenly you can’t progress fast anymore, because you have to debug with your DAW everytime. Maybe it’s even worse and you need to debug something that happens on load, so you can’t even debug with Bitwig’s plugin sandbox. Ultra slow workflow.

A great solution is to just make your own pseudo transport for processBlock. A class that has its own non-optional ppq, bpm, timeSig etc and you let it progress with every block manually whenever the playhead of the host doesn’t give you enough information. With that you can always debug everything, and as a nice side effect your temposync features even do something half-reasonable in Cantabile, so you need no edgecase handling.

Making transport information non-optional makes things solid.

3 Likes

slightly off topic, but you can work around this by adding some other plugin on another channel. This at least spools up the sandbox, you can attach to it and then load your plugin. Or I suppose you could change the settings of Bitwig to run plugins in process (not actually tested that). Frustrating either way…

Ah, I guess since its only available in the processBlock is why we have the AudioPlayhead::PositionInfo and why in the juce AudioPluginDemo we have the SpinLockedPosInfo class? JUCE/examples/Plugins/AudioPluginDemo.h at a8ae6edda6d3be78a139ec5e429dc57ef047e82a · juce-framework/JUCE · GitHub

Looking in the AudioProcessorPlayer, it does seem to provide its own internal Playhead which, unfortunately cannot have it’s PositionInfo updated to give us any real useable information (ie when i’m running in standalone i’m in the scenario where i have a playhead but nothing really useful from it)

I’m just trying to wrap my head around the terminologies and what should be responsible for what - and looking at the demo plugin it seems like we don’t even need to have a playhead to create some sort of timeline/transport in our plugin?

I guess I don’t really get if I should be subclassing my own custom playhead or if I should be modifying position info myself?

In terms of something more practical, I’m trying to make a plugin which monitors the daw’s transport state/position info if the plugin is set to “sync to host” or if its not synced, then do the same but using my own controls.

sorry if i’m kinda repeating myself with all this… for some reason i just can’t really wrap my mind around it.

EDIT: for some very pseudo code heres what i’ve got so far…

struct Transport
{
    enum class State
    {
        stopped,
        playing,
        recording
    };

    struct SpinLockedPosInfo
    {
        void set (const juce::AudioPlayHead::PositionInfo& newInfo)
        {
            const juce::SpinLock::ScopedTryLockType lock (mutex);

            if (lock.isLocked())
                info = newInfo;
        }

        juce::AudioPlayHead::PositionInfo get() const noexcept
        {
            const juce::SpinLock::ScopedLockType lock (mutex);
            return info;
        }

    private:
        juce::SpinLock mutex;
        juce::AudioPlayHead::PositionInfo info;
    };


    bool wantsHostPositionInfo = true;

    bool isUsingHostPositionInfo() const noexcept { return wantsHostPositionInfo; }

    void sendTransportChangeMessage (bool isPlaying, bool isRecording)
    {
        // send transport change message somewhere in the world...
    }

    void updatePositionInfo (juce::AudioPlayHead& ph, int numSamples)
    {
        auto pos = [&]
        {
            if (auto result = ph.getPosition())
                if (result.hasValue() && isUsingHostPositionInfo())
                    return *result;

            DBG ("No playhead position info available, using alternative values");
            return juce::AudioPlayHead::PositionInfo
            {
                // set pseudo playhead position info here using numSamples etc
            };
        }();

        spinLockedpositionInfo.set (pos);
    }
}

never messed with the spinlock, so idk what it’s purpose is here. i was just proposing a development workflow where a minimum amount of things is optional so you get consistent results in all build types and hosts. and yes, that’s kinda like running your own playhead