Architectural advice: standalone plugin GUI

Hello everyone! I’m approaching the GUI coding stage of my latest plugin project, and I have some concerns about the general architecture and class layout.

I would really like to be able to someday take just the GUI of my plugin and make a standalone app out of it, not connected to an AudioProcessor – essentially so that I can create a “remote control” app for other instances of my plugin.

I’ve created a Component-derived class ImogenGui, which is the top-level component representing the entire plugin GUI and containing all the child components, and my Editor just contains one of these objects.

The design question this brings up is, what’s the best way to manage the connection between the Processor and Editor, such that I can take the GUI and essentially “wrap” it in some other code to transmit parameter changes as OSC messages, instead of relaying them directly to an AudioProcessor object?

My first instinct was to create an abstract interface class ImogenGuiHandle, so that the ImogenGui can have a consistent API for getting and sending updates. In the plugin build, the Editor would inherit from ImogenGuiHandle, and for the standalone remote app version, I would create a wrapper version of ImogenGuiHandle that transmits all parameter changes to another instance of Imogen via OSC or some other networking.

My concern with this is that it would seem this precludes me from utilizing any of the AudioProcessorValueTreeState listeners/attachments… I would most likely need to implement individual functions for each parameterChange_send and parameterChange_recieve, yes?

Has anyone done anything like this before? Does anyone have any suggestions, or words of warning?

Thanks all :grinning:

I’ve been using an MVC model for my plugins recently which I think could help here.

Essentially, I use a juce::ValueTree to store all the data for the plugin. The processor can save/load it when needed and the editor can read and write to it as needed.

In doing so, the GUI side of my plugins have no idea about the processor, the only thing they interact with is the ValeuTree, so it would be fairly easy to remove the processor and have another class manage the saving/loading of the tree.

2 Likes

Interesting, thanks for the idea… So you don’t use an AudioProcessorValueTreeState, just a plain old ValueTree, I’m guessing set up with a child for each parameter, that would have to be linked to the parameter objects somehow…

And the editor can register ValueTree listeners for synchronous callbacks when a parameter changes?

The problem with a simple ValueTree is that the properties are accessed from different threads.
Even though ValueTree is in the name of the AudioProcessorValueTreeState, the only thing that makes parameters thread safe in JUCE is the AudioProcessorParameter.
You see it is regardless of APVTS or not. The parameters serve the two purposes:

  • communicate to the host
  • making interaction between host, processing and GUI thread safe

For the original problem: You don’t really need the Attachments here.
What you will do is you transport the API through your transport layer, in your case OSC.
The beginChangeGesture() and engChangeGesture() are relatively harmless. Just don’t leave them out since in that case the host and user input might fight over the actual value.

And what I would probably do is add a bool variable for gesture active, so in case you lose the conection during a gesture you can call endChangeGesture().

2 Likes

I do use an APVTS, I then attach an additional ValueTree to the APVTS’s state member with all my GUI properties in it.

Sorry I missed the part about parameters… that might require some extra work to be able to read and write to parameters without the use of APVTS. I wonder if you could instead attach a juce::Value to those parameters in the APVTS which your editor can use. You might lose some functionality that way and as @daniel has pointed out you have the threading issues to worry about.

1 Like

What if I do use an APVTS, and in the “remote” version, I just create a “dummy” AudioProcessor that does nothing except hold the parameters, and then I’d set up various listeners to do the messaging, etc.

That’s a great idea! I’ll have to look into detecting when the connection is lost… (or should it just be a time-out sort of thing?)

Thank you both for your advice!

I don’t think you need a dummy parameter, because the remote has no access to the audio thread nor the host.
I think it is more like an alternative GUI.

A possible protocol:

  • Get value (for display)
  • BeginChangeGesture
  • SetValueNotifyingHost
  • EndChangeGesture
class ParameterAdapter : public juce::OSCReceiver::Listener,
                         private juce::AudioProcessorParameter::Listener
{
public:
    ParameterAdapter (juce::RangedAudioParameter& p) : parameter (p)
    {
        parameter.addListener (this);
    }

    ~ParameterAdapter()
    {
        parameter.removeListener (this);
    }

    void oscMessageReceived (const juce::OSCMessage &message) override
    {
        if (/* message is user touches device */)
        {
            gestureInProgress = true;
            parameter.beginChangeMessage();
        }
        else if (/* message is user releases device */)
        {
            parameter.endChangeMessage();
            gestureInProgress = false;
        }
        else if (/* user sets a value */ && gestureInProgress)
            parameter.setValueNotifyingHost (value);
    }

    void parameterValueChanged (int parameterIndex, float newValue)
    {
        // send value to OSC
    }
    
private:
    juce::RangedAudioParameter& parameter;
    bool gestureInProgress = false;
};

I don’t think there is more to it…

You are right, in OSC you probably wouldn’t know when the connecion is dropped.

I would tell a UDP joke but you might not get it. :wink:

1 Like

Ah, this makes sense!

So in either version, the ImogenGui knly needs to know about its ParameterAdapters, which in the plugin build can just directly link each parameter to the processor, yes?

So I could theoretically still use an APVTS in my plugin processor, I would just have to manually manage the linkage from GUI components to parameters, yes?

Yes, that was my thinking.

I started using juce when the APVTS was introduced so I am also used to use it for everything. But nowadays I try to connect directly to the parameters to allow more flexibility when I reuse those patterns in other projects, that might not use the APVTS.
Thanks to the new ParameterAttachment it is much easier now.

1 Like

I’ve always used the APVTS in my own projects so far. I actually don’t think I’ve tried a project without it, so maybe it would be a good exercise just to try it…

One more interesting conundrum… I’m using integer IDs for each of my parameters, and previously I had them declared as an enum, ImogenAudioProcessor::ParameterID. If I want to be able to use this enum from my “processor-less” GUI version, then should I be declaring this enum in some sort of global header that’s not a part of the AudioProcessor class?

OK, I think I’m getting somewhere now…

This is definitely teaching me a lot about object oriented programming architecture and inheritance!

I’ve got the class ImogenGUI set up to be the main component for the plugin’s GUI, containing all the child components representing parameters. ImogenGUI uses this abstract interface to communicate gui → processor:

struct ImogenGuiHandle
{
    virtual ~ImogenGuiHandle() = default;
    
    // called by the GUI to send parameter changes to the processor
    virtual void sendParameterChange (int paramID, float newValue)=0;
    
    virtual void sendEditorPitchbend (int wheelValue)=0;
    
    virtual void sendMidiLatch (bool shouldBeLatched)=0;
    
    virtual void loadPreset   (const juce::String& presetName)=0;
    virtual void savePreset   (const juce::String& presetName)=0;
    virtual void deletePreset (const juce::String& presetName)=0;
};

And then I made an ImogenGuiHolder class, which implements this interface and actually contains an ImogenGUI object:

ImogenGuiHolder is the type that the processor uses to send out parameter change callbacks to the editor in the plugin build.

So then, I’m able to easily create an AudioProcessorEditor that inherits from ImogenGuiHolder:

and a MainComponent that also inherits from ImogenGuiHolder, but needs no actual bindings to an AudioProcessor object:

The idea is that the MainComponent will also contain/be a juce::OscReciever and simply parse & forward the appropriate parameter change messages to its ImogenGUI object, and then its implementation of the ImogenGuiHandle interface will send out parameter changes as OSC messages, instead of sending them directly to an AudioProcessor.

How does all of this look so far? Any general suggestions? Thanks for all the help :slightly_smiling_face: