How to handle non-DSP parameters that influence others

Hi there,

I’m new to iPlug2, and over the past couple of weeks managed to build my first UI for a project I’m working on:

signal-2025-02-23-233332_002

One of the features of this EQ UI is that the currently selected handle is stored in a parameter called “activeBandId”. The issue is that this is then available through the host when compiled as a VST3 plugin. I really don’t want it to be since then a user could mess with the operation of the GUI.

How could I create a parameter that is used by the UI but is not exposed to the DSP? The no parameter option also doesn’t really work since then it doesn’t take part in the main update cycles.

I don’t really want to hold such a parameter multiple times, since in the above interface the curve drawing is one custom IControl, the bottom section with 8 rounded boxes that change when you click, is another, and the 3 knobs to the left are another, and they all need to know what the currently active value is.

What do people do in iPlug2 to reliably track such state that is shared between multiple IControls, but is not exposed to the DSP and such that changing this value from one of multiple IControls, safely, and efficiently, notifies and updates all the other ones.

I’m thinking of extending this concept even further to have basically DSP-related parameters in one set, and UI-only parameters in another, but iPlug2 doesn’t really seem to have this as a concept, unless I’m missing something.

THANKS!

Should activeBandId be stored in the plugin state and recalled in presets/daw projects? If so, and you don’t want it to be a somewhat hidden parameter you can declare the parameter with the flag kFlagCannotAutomate. But perhaps it shouldn’t be a parameter at all. In this case you could store it as a member variable eg. mActiveBandId in your plugin class and serialize it in the state (See the IPlugChunks example). When the UI opens you can pass a reference to mActiveBandId to the each control that needs it (you might need to make custom controls that inherit the base controls for this).

1 Like

Ah, so it’s safe to pass references to member variables within iPlug2?

I was under the impression looking at the code that with all the atomics you are using that there were threading issues doing that?

Or does the UI always run on a single thread, and therefore any data contained exclusively inside the GUI is safe to access using pointers and references?

The next question I’d then ask is how could you ensure things update correctly? There is a lot of thought and work that’s gone into the IParam interface and ensuring all that works correctly, but if I wanted to do it myself, how could I, say, get an IVKnobControl initialized with kNoParameter to say, update any other control (or, even just simply display it’s value)?

The system seems to specifically designed to work with IParams, that when you move away from that, you are basically having to re-implement all of that manually? Is that correct? It’s OK if so, I’m just not sure if there is a “recommended/standard” way to do this.

Thanks! You’ve done amazing work here, it’s really a great library.

One simple thing I noticed trying to do this for say IVKnobControl is that if you use kNoParameter you cannot display the value.

So I tried overriding const IParam* GetParam(int valIdx = 0) const to return a new local IParam I defined on the class instance, but that doesn’t get used. I’m just not sure why.

BUT… assuming I could get that to work, I think this might be a way around the issue.

i.e. store a local IParam for the “hidden” parameter to define all the stuff it needs, and work with that. I’ll let you know how that goes.

Ah bugger, it cannot work because const IParam* GetParam(int valIdx = 0) const; on IControl is not a virtual function.

If I make GetParam virtual, and implement this, it shows the default and it doesn’t update, but it’s a start:

class etheoryIVKnobControl : public IVKnobControl
{
  using IVKnobControl::IVKnobControl;
public:
  etheoryIVKnobControl(const IRECT& bounds, int paramIdx,
  const char* label = "",
  const IVStyle& style = DEFAULT_STYLE,
  bool valueIsEditable = false, bool valueInWidget = false,
  float a1 = -135.f, float a2 = 135.f, float aAnchor = -135.f,
  EDirection direction = EDirection::Vertical, double gearing = DEFAULT_GEARING, float trackSize = 2.f)
  : IVKnobControl(bounds, paramIdx, label, style, valueIsEditable, valueInWidget, a1, a2, aAnchor, direction, gearing, trackSize)
  {
    param.InitDouble("Freq", 100., 10., 22000., 0., "", 0, "", IParam::ShapeExp(), IParam::kUnitFrequency);
  }

  const IParam* GetParam(int valIdx = 0) const
  {
    return &param;
  }

private:
  IParam param;
};

Thoughts or am I crazy?

But in essence this is the pattern I’m trying to make. Have an IControl define a kNoParameter paramIdx and then instead store an IParam inside the class and use that instead. It then doesn’t take part in the main parameter update cycle, so you can then do whatever you like with it.

My overall goal is to have a reconfigurable UI with modular components, and then only expose some parameters to the host, that are configurable inside the plugin itself. Like macro-style parameters.

I got it working:

class etheoryIVKnobControl : public IVKnobControl
{
  using IVKnobControl::IVKnobControl;
public:
  etheoryIVKnobControl(const IRECT& bounds,
  const char* label = "",
  const IVStyle& style = DEFAULT_STYLE,
  bool valueIsEditable = false, bool valueInWidget = false,
  float a1 = -135.f, float a2 = 135.f, float aAnchor = -135.f,
  EDirection direction = EDirection::Vertical, double gearing = DEFAULT_GEARING, float trackSize = 2.f)
  : IVKnobControl(bounds, kNoParameter, label, style, valueIsEditable, valueInWidget, a1, a2, aAnchor, direction, gearing, trackSize)
  {
    param.InitDouble("Freq", 100., 10., 22000., 0., "", 0, "", IParam::ShapeExp(), IParam::kUnitFrequency);
  }

  void SetDirty(bool triggerAction = true, int valIdx = kNoValIdx) override
  {
    param.Set(param.FromNormalized(GetValue()));
    IVKnobControl::SetDirty(triggerAction, valIdx);
  }

  // relies on changing the GetParam of IControl in iPlug2 to be virtual
  const IParam* GetParam(int valIdx = 0) const override
  {
    return &param;
  }

private:
  IParam param;
};

The only change to the library now is setting GetParam to virtual.

This is basically also exactly what I wanted. The IControl is now completely self-contained, and can drive parameters that it owns directly, without needing to go through any of the normal update cycles, making it safe to attach and unattach from the interface dynamically. Which would allow me to do dynamic modular interfaces.

Unfortunately this breaks: PromptUserInput since it calls GetParamIdx which returns kNoParameter which then prevents the prompt from appearing and GetUI()->PromptUserInput takes valIdx as an argument. A modification to resolve this would be instead to pass through the param instance itself, instead of assuming the parameter exists on the IGraphics GUI. But that is getting into a system redesign.

@olilarkin are you open to a PR that suggests design changes to make this work, or is this not interesting to you? I would design this to be backwards compatible with what’s currently there.

The most obvious fix would be to add a new signature:

void IGraphics::PromptUserInput(IControl& control, const IRECT& bounds, const IParam& param)

That can be called by:
void IGraphics::PromptUserInput(IControl& control, const IRECT& bounds, int valIdx)

And therefore allow the user to call the parameter version directly. The reason this should work is that PromptUserInput only reads the param, it doesn’t set it.

You’d then also need new signatures for:
void IGraphics::CreatePopupMenu(IControl& control, IPopupMenu& menu, const IRECT& bounds, int valIdx)
and
void IGraphics::DoCreatePopupMenu(IControl& control, IPopupMenu& menu, const IRECT& bounds, int valIdx, bool isContext)
and then to figure out what mPopupMenuValIdx would now mean, as it wouldn’t mean what it did before…

I found another simple solution that works.

If you make GetParamIdx virtual, then this solves the prompt issue:

class etheoryIVKnobControl : public IVKnobControl
{
  using IVKnobControl::IVKnobControl;
public:
  etheoryIVKnobControl(const IRECT& bounds,
  const char* label = "",
  const IVStyle& style = DEFAULT_STYLE,
  bool valueIsEditable = false, bool valueInWidget = false,
  float a1 = -135.f, float a2 = 135.f, float aAnchor = -135.f,
  EDirection direction = EDirection::Vertical, double gearing = DEFAULT_GEARING, float trackSize = 2.f)
  : IVKnobControl(bounds, kNoParameter, label, style, valueIsEditable, valueInWidget, a1, a2, aAnchor, direction, gearing, trackSize)
  {
    auto freqFormatter = [](double val, WDL_String& str, IParam* parent)
    {
      const double onethousand = parent->FromNormalized(parent->ToNormalized(1000.));
      const bool gte1k = val >= onethousand || val >= 1000.;
      const char* units = gte1k? "kHz" : "Hz";
      if (gte1k) val *= 0.001;
      const int displayPrecision = parent->GetDisplayPrecision();
      str.SetFormatted(MAX_PARAM_DISPLAY_LEN, "%.*f %s", displayPrecision, val, units);
    };

    param.InitDouble("Freq", 100., 10., 22000., 0., "", 0, "", IParam::ShapeExp(), IParam::kUnitFrequency);
    param.SetDisplayFunc(std::bind(freqFormatter, std::placeholders::_1, std::placeholders::_2, &param));
    param.SetDisplayPrecision(2);
  }

  void SetDirty(bool triggerAction = true, int valIdx = kNoValIdx) override
  {
    param.Set(param.FromNormalized(GetValue()));
    IVKnobControl::SetDirty(triggerAction, valIdx);
  }

  int GetParamIdx(int valIdx = 0) const override { return 0; }
  const IParam* GetParam(int valIdx = 0) const override
  {
    return &param;
  }

private:
  IParam param;
};

And you can then edit the value. And it works… Kinda… it now sets the global param at index 0 as you change the knob, which is wrong. But it’s a start.

If I am understanding your concept correctly, you are sharing one Frequency (Gain & Q) parameter among 7 bands? What you see then, if you switch off GUI (parameter view) in a DAW, or how can you automate such plugin?

In my case the way I want to do it is via macro controls in the plugin. Most of what the plugin needs to do will be via the GUI. But there will be a fixed number of macro parameters that are configurable in the GUI to be mapped to any of the other parameters. Some commercial synthesizers work this way. The reason I need this is that the tool I am building is a modular engine, so it could have 1 or 10 or 100 different nodes in the GUI. And that makes a fixed parameter set impossible. So I’ll solve that with a fixed set of say 16 daw parameters the user can assign via the GUI.

In this particular case all the parameters for each band like freq/Q/gain exist as daw parameters. I just simply wanted a gui parameter to store the previously selected band to display on the knobs. Therefore I need a “gui-only” parameter that can take part in updating other parts of the GUI. Something that’s currently not easy with the kNoParameter mechanism. I have a PR I’m about to propose that solves this via the concept of local IControl parameters. Where you can store some params directly inside an IControl and others on the global level. Its working well in local testing and would solve the above.