An elegant solution to input/output gain linking?

My plugin has an input gain and an output gain, and like a lot of plugins, I’d like to link these so when the user, say, increases input gain to +6db, then output gain goes to -6db to compensate.
However I run into this problem:

void PluginProcessor::parameterChanged (const juce::String &id, float newValue)
    if(id == param::toID(param::PID::InputGain))
            //this call will then trigger parameterChanged()
    else if(id == param::toID(param::PID::OutputGain))
            //and so will this

When parameterChanged() gets called, it then sets the value of the input/output gain respectively, which then calls parameterChanged() again, which updates the input/output gain etc.

How can I avoid this loop of calling parameterChanged() over and over?



It’s likely better to not do the linking in the parameter change handlers, but instead in the UI. So when the slider moves, you could change both parameters depending on the link setting. Basically it’s better seen as a feature of the UI, not of the processor itself.


But I’d still want the changes to be consistent even if someone changes the parameter directly in their daw. i.e if the Editor is closed and they change the input gain, I’d want the output gain to change respectively.

And what do you do when automation is applied to both parameters?

Coupled parameters are always tricky and when automation or host undo/redo comes into play will always bite you in the foot.

What you and your users probably really want is input/output gains that are always coupled, along with an additional output gain. There’s almost never a point in changing only the input gain without compensating the output. However, changing output gain alone is useful. So design the DSP in a way where the input gain is automatically compensated, and output gain is applied on top. You can still present it as coupled knobs in the UI if you absolutely must (if your UI has scratches and screws for example). But that way you get a behavior that feels right. For the rare case where someone would want to change only input gain, you can let 'em do that with a modifier key and change two parameters at once. But most of the time headaches will be saved that way.


So if I understand correctly here is some pseudocode:


//processing ...

if (coupled)

This makes sense to me, but now I will just have to wrestle with the gui code for the output gain slider :laughing:

I feel like it might be sufficient just having the output gain respond to the input for compensation, rather than also the other way around.

I don’t think users would want the output gain to change their carefully-tuned input gain for non-linear effects like compression or saturation. Also, on the non-linear topic, a change in the input gain would likely need a non-linear change in the output gain to maintain the same loudness, rather than simply outputGain = -inputGain. If your processing is all linear, then adding gain and removing it after the processing won’t do anything.

If you decide the output-to-input compensation isn’t needed, this could let you grey-out the output slider when compensation is active, which would essentially bypass any output gain automation. This would also avoid the parameterChanged() feedback loop.


I think you are correct! A much simpler solution. I have done as you have said, when gain compensation is on, the output slider is disabled and can’t be interacted with, but still displays the opposite of the input gain because it is attached to the output gain parameter and I update its value in parameterChanged(). I will have to test with automation though to see how it behaves.

For anyone else finding this thread:
my plugin was failing auval because it didn’t like the parameter linking, so I had to mark the gain compensation parameter as “Meta”. You can do this simply with this struct (found on a different forum post):

    template <typename ParameterType>
    struct MetaParameter : public ParameterType
        static_assert (std::is_base_of<juce::AudioProcessorParameter, ParameterType>::value,
                       "ParameterType must be a juce::AudioProcessorParameter.");
        using ParameterType::ParameterType;
        bool isMetaParameter() const override { return true; }

param = std::make_unique<MetaParameter<YouParamType>>(...args...);

No longer necessary to do it this way. You should use the AudioParameter options that all ctors offer

can you provide an example?

    AudioParameterFloatAttributes attributes = AudioParameterFloatAttributes().withAutomatable(automatable).withMeta(meta);
    AudioParameterFloat* parameter = new AudioParameterFloat(paramID, paramName, range, defaultValue, attributes);

I would not make the wet gain knob visibly turn with input gain knob changes, because then the output gain parameter can’t be automated anymore and stuff like that, without breaking this functionality. instead i’d like to suggest to make these changes happen internally only. so if there is input gain applied, then right before it’s time to apply the wet gain you apply 1 / inputGain to the signal. that way you get a nice little gain compensation approximation for all kinds of nonlinear effects, like saturation, but without affecting how the wet gain parameter is being used by the user. by the way i say wet gain and not out gain on purpose, because if you ever plan to add a mix knob, you’ll notice that the inverse input gain belongs into the wet-layer only, which implies that an output gain, that would also gain the dry signal, is not related to it