Parameters - Best Practice


#1

EDIT: It looks like it would be better to WAIT before using AudioProcessorValueTreeState. One of the major issue at the moment (for me at least) is that AU parameters are listed in the DAW in random order. Check this AU Parameter Order
Plus, it seems like the JUCE team is still working on the new system: We are removing AudioProcessor-based parameter management
I’ll stick with my old method using just AudioProcessorParameter for now. I’ll probably update this thread if things change.


Sorry for the long post, but I think there’s a need for a much more detailed tutorial for complex plugins with many parameters.

I’ve finally decided to test JUCE 5 and it seems like many nice things were added, as long as many breaking changes. I don’t plan to update my old plugins for now (JUCE 4 mess was enough…), but I’m considering releasing new stuff with JUCE 5. Looking at the examples and at the new tutorials, I see very different ways of dealing with parameters and setState/getState methods.

My old plugins are all using AudioProcessorParameter and something like addParameter (Param = new FloatParameter (0, "Parameter", 0, *this)); in the constructor. So I have my nice FloatParameter class were I deal with everything, with a big switch on the parameter index. The UI is synchronized with a timer and my custom sliders/buttons are linked to an AudioProcessorParameter*. It works, it’s not elegant and probably not really efficient, but it works.

AudioProcessorValueTreeState
So, I want to improve how I deal with parameters, and the AudioProcessorValueTreeState looks very promising but more confusing. This is what I found out so far.

Adding Parameters
It seems like we can only use AudioProcessorParameterWithID* with the createAndAddParameter method. Why? Why can’t we use AudioParameterChoice and AudioParameterBool? It’s not a major issue, but it would be nice to show the correct representation of the parameter (button, choice list) in the DAW. Just found out that I need to declare the parameter as discrete.

So, we have to do something like that for a bool parameter:

params.createAndAddParameter ("bypass",
                              "Bypass",
                              String(),
                              NormalisableRange<float> (0.0f, 1.0f, 1.0f),
                              0.0f,
                              [](float value) { return value < 0.5 ? "on" : "off"; },
                              [](const String& text)
                              {
                                 if (text.toLowerCase() == "on") return 0.0f;
                                 if (text.toLowerCase() == "off") return 1.0f;
                                 return 0.0f;
                              },
                              false, // meta
                              true,  // automatable
                              true); // discrete (for bool and choice params)

For a standard float parameter intead:

params.createAndAddParameter ("input",
                              "Input Gain",
                              String(),
                              NormalisableRange<float> (0.0f, 4.0f, skewGainConvertFrom0To1, skewGainConvertTo0To1),
                              0.5f,
                              parameterDecibelToString,
                              parameterDecibelToValue);

I’m using custom methods for skew and string/value returns. I really love this approach.

(get/set)StateInformation
Very easy now compared to the old way.

void getStateInformation (MemoryBlock& destData) override
{
    auto state = parameters.copyState();
    ScopedPointer<XmlElement> xml (state.createXml());
    copyXmlToBinary (*xml, destData);
}

void setStateInformation (const void* data, int sizeInBytes) override
{
    ScopedPointer<XmlElement> xmlState (getXmlFromBinary (data, sizeInBytes));
    
    if (xmlState != nullptr)
        if (xmlState->hasTagName (parameters.state.getType()))
            parameters.replaceState (ValueTree::fromXml (*xmlState));
}

Parameter Changes in Processor

Looking at the DSPModulePluginDemo I see that there’s a call to a updateParameters() before the actual processing. For some parameters it’s actually much safer to check every time before the processing. But I don’t think that should be the case for all parameters.
For example:

void updateParameters()
{
    if (type != *params.getRawParameterValue("type"))
    {
        type = *params.getRawParameterValue("type");
        thingThatNeedsType.changeType(type);
    }
}

So, some questions:

  • is AudioProcessorValueTreeState stable enough to be used in a release? Is it going to be updated and stick around in the future, or is ROLI working on yet another system?
  • will it be possible in the future to add proper bool and choice type parameters to AudioProcessorValueTreeState? we can just declare the parameter discrete
  • is there a better way to deal with parameter changes in the processor (not in the UI) other than a updateParameters() before processing? I see there’s a listener with a parameterChanged method. Is this method thread-safe? Is it called in any case a parameter has been changed (for example a default value, automation etc…)?
  • so basically what I’d like to know is, is it worth spending time to deal with AudioProcessorValueTreeState or should I stick with my old method with just AudioProcessorParameter*?

Would be nice if someone from ROLI (@fabian?) could create a dedicated thread/tutorial with a better explanation of the parameters system.


#2

I’ve just edited the post answering the bool/choice parameters question. I didn’t see that you could declare the parameter as discrete. This seems to work well on Logic and Live (AU).


#3

An overhaul to the whole complex was announced a few weeks ago, including best practise tutorials and further features.
AFAIK t0m is currently working on AudioParameterGroups.
And Fabian should be back in about a month…

Another addition for me would be to share the textToValue and valueToText via the SliderAttachment between the parameter and the slider TextBox. It is a bit moot, that it has to be done in two places, but not really a biggie.

Cheers for the good summary!


#4

Thanks. I haven’t looked at attaching parameters to UI elements yet. In my old system, all my UI elements have a pointer to their parameter, I guess it should be something similar with the new system.

Anyway, good to know, it looks like I should probably wait before using AudioProcessorValueTreeState until everything is sorted.
I’ve also checked this thread AU Parameter Order and as it is right now it doesn’t make sense to use it and/or AudioProcessorParameterWithID. I will update the thread with a warning.

I’m still on JUCE 4 (actually 4.1), and although it is tempting to update to JUCE 5, this parameter situation reminds me of the multi-channel mess. I learned my lesson back then.


#5

I use the SliderAttachment, ComboBoxAttachment etc. for everything, so you don’t need to implement any callback yourself. GUI and state are always in sync.

The only thing is how you use the value in the processor. You have the choice reading the value each processBlock (from a float* or getter), or an additional inheriting AudioProcessorValueTreeState::Listener and register for change callbacks. In some threads there was discussion about thread safety, I don’t know the outcome, but personally I had no problems so far.

Although I expect the API to stay stable with minor changes, I completely get what you mean… Fingers crossed :wink:


#6

Cool, I’ll play with the listener when I’ll have more time. It looks like I’ll be using my old system for a while…


#7

Ok, I’m still testing things. I’ve tried the listener and it works as it should, but I have an issue with a parameter with a specific range. I’m still wrapping my head around NormalisableRange and lambdas.
I have a delay parameter in ms that goes from 0ms to 100ms:

params.createAndAddParameter("delay",
                             "delay",
                             String(),
                             NormalisableRange<float> (0.0f, 0.100f, 0.001f),
                             0.020f,
                             [](float value) { return String(value*1000) + "ms"; },
                             [](const String& text) { return text.getFloatValue()/1000.0f; });

params.addParameterListener("delay", this);

The problem is that the lambda that returns the string for the parameter value (valueToTextFunction) is called everytime I move the parameter. The listener instead triggers only when the parameter value is snapped and converted. From juce_AudioProcessorValueTreeState.cpp:82

void setValue (float newValue) override
{
    newValue = range.snapToLegalValue (range.convertFrom0to1 (newValue));

    if (value != newValue || listenersNeedCalling)
    {
        value = newValue;

        listeners.call (&AudioProcessorValueTreeState::Listener::parameterChanged, paramID, value);
        listenersNeedCalling = false;

        needsUpdate.set (1);
    }
}

How can I limit the lambda to update the string in the same way as the setValue/listener call?

EDIT: adding roundToInt in the lambda seems to work most of the time, but there are still slightly changes that are not picked up by either the listener or the lambda.