AudioProcessorValueTreeState: initializing parameter with value=0


#1

When adding Parameters to my AudioProcessorValueTreeState my AudioProcessorValueTreeState::Listener::parameterChanged() gets called for every non-zero default parameter value.
There seem to be some kind of “optimization” preventing the initial setting of value=0.

The parameter class is probably constructed with a default value of 0, and explicitly setting it to 0 again does not notify the processor class.

Is this a bug or a feature? Or in other words: should I enforce notifying the processor with an initial parameter setting of zero by myself or will you allow notifying the processor even when the parameter is constructed with a default of 0. (The current behaviour seems to be inconsistent to me.)

Thanks,
raketa


#2

I think it’s just that parameter values are set to their default values shortly after construction. You receive the parameter changed notifications for every case where the default value is not equal to zero (which is the initial value).

It’s only an “optimisation” in the sense that if a user of a plug-in triggered a setValue call which didn’t actually change the value then you wouldn’t get a callback - which is the desired behaviour.


#3

Thanks t0m,

thats pretty much what I say in the OP. But the question remains: Will you change the behaviour so AudioProcessor::parameterChanged() is always called initially? Or should I work around that feature? Its pretty obvious that all algorithm parameter need to be initially set, wether they are zero or not.


#4

You can achieve what you want with something like

    parameters.createAndAddParameter ("gain",       // parameterID
                                      "Gain",       // parameter name
                                      String(),     // parameter label (suffix)
                                      NormalisableRange<float> (0.0f, 1.0f),    // range
                                      0.5f,         // default value
                                      [](float value) { return String (value); },
                                      nullptr);

    parameters.addParameterListener ("gain", this);
    parameters.getParameter ("gain")->setValue (0.5);

where you explicitly set the parameter after adding a listener. This is a clearer solution than making parameters broadcast their values whenever a new listener is added.


#5

Thanks, T0m,

are you sure
parameters.getParameter ("gain")->setValue (.0);
will trigger AudioProcessor::parameterChanged()?

Cheers,
raketa.


#6

I didn’t try that particular example. What I did was load some saved state (from REAPER) where the saved parameter value was 0.5. In this case you don’t get a parameterChanged callback because the parameter hasn’t changed from it’s default value. If you set the value explicitly then you will get the callback.


#7

But its all about promoting the default value of zero initially: I guess the same mechanism will prevent promoting when explicitly setting the default value of zero to zero again.


#8

Sorry - I’m a bit confused. When are you receiving your existing parameterChanged callbacks?

If you take the example code and comment out the line

parameters.getParameter ("gain")->setValue (0.5);

then parameterChanged won’t be called when you construct your AudioProcessor, irrespective of the default value.


#9

in my AudioProcessor constructor I create AudioProcessorValueTreeState managed parameters with createAndAddParameter() followed by addParameterListener().

For all non-zero default parameters I receive a AudioProcessorValueTreeState::ListenerparameterChanged() notification.


#10

Which host are you using to load your plug-in?

I’ve tried the code I posted (a very slightly modified version of the code from the tutorial at https://www.juce.com/doc/tutorial_audio_processor_value_tree_state), in REAPER and the JUCE Demo Host. Neither produce a parameterChanged callback before I change the parameters manually.


#11

Which host are you using to load your plug-in?

Doesn’t seem to matter: macOS/Windows VST3 in recent Cubase or Reaper

On the other hand I can reproduce the exact same behaviour with a modified https://www.juce.com/doc/tutorial_audio_processor_value_tree_state:

class TutorialProcessor  : public AudioProcessor, public AudioProcessorValueTreeState::Listener
{
    virtual void parameterChanged (const String& parameterID, float newValue) override {
		std::clog<<__FUNCTION__<<" "<<parameterID<<" "<<newValue<<std::endl;
	};
public:

    //==============================================================================
    TutorialProcessor()
    :   parameters (*this, nullptr)
    {
        parameters.createAndAddParameter ("gain",       // parameterID
                                          "Gain",       // parameter name
                                          String(),     // parameter label (suffix)
                                          NormalisableRange<float> (0.0f, 1.0f),    // range
                                          0.0f,         // default value
                                          nullptr,
                                          nullptr);

		parameters.addParameterListener("gain", this);

        parameters.createAndAddParameter ("invertPhase", "Invert Phase", String(),
                                          NormalisableRange<float> (0.0f, 1.0f, 1.0f), 0.0f,
                                          invertPhaseToText,    // value to text function
                                          textToInvertPhase);   // text to value function
        
        parameters.state = ValueTree (Identifier ("APVTSTutorial"));
    }

while setting the default value to 0. does not trigger virtual void parameterChanged(), setting it to something else does trigger it.


#12

… and more: its not compared against zero, its compared against its minimum.

so changing it to

   parameters.createAndAddParameter ("gain",       // parameterID
                                      "Gain",       // parameter name
                                      String(),     // parameter label (suffix)
                                      NormalisableRange<float> (0.5f, 1.0f),    // range
                                      0.6f,         // default value
                                      nullptr,
                                      nullptr);

triggers the notification while

   parameters.createAndAddParameter ("gain",       // parameterID
                                      "Gain",       // parameter name
                                      String(),     // parameter label (suffix)
                                      NormalisableRange<float> (0.5f, 1.0f),    // range
                                      0.5f,         // default value
                                      nullptr,
                                      nullptr);

does not.


#13

now, a workaround by setting the value manually can’t be done from the constructor, because it directly calls the virtual notification.


#14

Aaah, OK. The VST and VST3 wrappers are handling how they set the default parameters differently. I was testing with VST and no callbacks are received until either the host loads some saved state or the plug-in user changes a parameter. The fact that you get any callbacks before then is a “feature” of the VST3 wrapper. However, setting the default value explicitly as in my code example should provide you with the callbacks you want in any case.


#15

Thanks, t0m,

well, as mentioned: not exactly, since setting the values explicitly is calling the virtual notification from the constructor which prevents inheriting from such a class.

Cheers,
raketa


#16

I therefor propose following change to the VST3 wrapper: initialize the inherited valueNormalized:

line 210:

struct Param  : public Vst::Parameter
{
    Param (AudioProcessor& p, int index, Vst::ParamID paramID)  : owner (p), paramIndex (index)
    {
        info.id = paramID;
        toString128 (info.title, p.getParameterName (index));
        toString128 (info.shortTitle, p.getParameterName (index, 8));
        toString128 (info.units, p.getParameterLabel (index));

        const int numSteps = p.getParameterNumSteps (index);
        info.stepCount = (Steinberg::int32) (numSteps > 0 && numSteps < 0x7fffffff ? numSteps - 1 : 0);
        info.defaultNormalizedValue = p.getParameterDefaultValue (index);
        jassert (info.defaultNormalizedValue >= 0 && info.defaultNormalizedValue <= 1.0f);
        info.unitId = Vst::kRootUnitId;

        // is this a meter?
        if (((p.getParameterCategory (index) & 0xffff0000) >> 16) == 2)
            info.flags = Vst::ParameterInfo::kIsReadOnly;
        else
            info.flags = p.isParameterAutomatable (index) ? Vst::ParameterInfo::kCanAutomate : 0;
    }

insert initialization of the inherited valueNormalized:

struct Param  : public Vst::Parameter
{
    Param (AudioProcessor& p, int index, Vst::ParamID paramID)  : owner (p), paramIndex (index)
    {
        info.id = paramID;
        toString128 (info.title, p.getParameterName (index));
        toString128 (info.shortTitle, p.getParameterName (index, 8));
        toString128 (info.units, p.getParameterLabel (index));

        const int numSteps = p.getParameterNumSteps (index);
        info.stepCount = (Steinberg::int32) (numSteps > 0 && numSteps < 0x7fffffff ? numSteps - 1 : 0);
        info.defaultNormalizedValue = p.getParameterDefaultValue (index);
        jassert (info.defaultNormalizedValue >= 0 && info.defaultNormalizedValue <= 1.0f);
        info.unitId = Vst::kRootUnitId;

        // is this a meter?
        if (((p.getParameterCategory (index) & 0xffff0000) >> 16) == 2)
            info.flags = Vst::ParameterInfo::kIsReadOnly;
        else
            info.flags = p.isParameterAutomatable (index) ? Vst::ParameterInfo::kCanAutomate : 0;
        valueNormalized = -1.;
    }

and initialize it to something invalid (outside the normalized range) to indicate it has not been set yet.
This will allow to initially notify the parameter about its new value:

line 236:

    bool setNormalized (Vst::ParamValue v) override
    {
        v = jlimit (0.0, 1.0, v);

        if (v != valueNormalized)
        {
            valueNormalized = v;
            owner.setParameter (paramIndex, static_cast<float> (v));

            changed();
            return true;
        }

        return false;
    }

#17

Would you consider the above change to commit?


#18

I don’t think your change will improve things significantly for JUCE users - the behaviour for VSTs is already different (as evidenced by the fact that I struggled to reproduce the behaviour you were seeing) so if we were going to fix it we would want to harmonise the behaviour across AU, VST, VST3 and AAX. I’m also not that keen on initialising the parameter to an invalid value.

This is something we’ll look at, but I can’t promise anything happening soon.


#19

Yes, I agree: it needs to be fixed so its working consistently across all plugin formats. But actually thats what I was expecting in the first place. Thats not a nice to have. Thats the solely reason to use an abstraction framework.

And yes, there might be better solutions.
But why would be initializing the parameter to an arbitrary valid value be more adequate? Initializing it to a valid value hides the fact that it never was intentionally set at all.


#20

right, I just bumped into the same issue with a vst2 (when loading a state where some params where set to the default/min value).

I just needed the parameterChanged() of my processor to be triggered here, so the following did the trick.

for (auto p : getParameters())
{
    if (auto param = dynamic_cast<AudioProcessorParameterWithID*> (p))
        parameterChanged (param->paramID, *state.getRawParameterValue (param->paramID));
}

note that the SliderAttachment/ComboBoxAttachment work fine because they call AttachedControlBase::sendInitialUpdate().