Is it smart to use APVTS::Listener::parameterChanged with many parameters

Hello,
APVTS::Listener::parameterChanged takes as parameter const String &parameterID.
So the only way to recognise which parameter is changed I need to make if statement, like:

 if(parameterID == myParameterID)
     //do something

I am not experienced programmer, but it seems that comparing Strings isn’t too much efficiency.
And if I have many parameters, for example 30, I need to make 30 times else if statement, which is also known less efficiency than Switch() statement which I can’t use with Strings.

And one more strange thing for me is that I need to add listener separately for each parameter. I have all parameters defined in myAPVTS, so it seems to be obvious to add listener for the whole APVTS object.

Do I understand it in wrong way? Or is there any fancier methods to handle parameters changes?

Why I have that issue? The problem I found today. I had parameters attached to my sliders only. So accidentally I found out that when I set some automations in Logic Pro X, it worked only when plugin was opened. But when I closed plugin automation didn’t take effect.
So I made some investigation in the code, and found out that it’s obvious that if I have parameters only attached to sliders in plugin editor, and that is the only place where I call my processor functions in sliderValueChanged(), so when I close editor the parameters are attached to nothing.

That’s why I found that I need to make some attachment in processor, not editor. That’s why I use APVTS::Listener. Maybe it should be done in some better way?

For any help, great thanks in advance :slight_smile:

That is actually a good question.

When you call addParameterListener() on the APVTS, it is actually not adding a listener to the APVTS itself, but to the AudioProcessorParameter. So you get only a callback for that parameter, which is the most efficient way.

You are right, if you add your processor as listener to multiple parameters, they all call the same callback, so it is up to you to pull it out again, which you rightfully say is overhead, that should be avoided.

The solution is to have a listener object for each parameter, that way you always know, what parameter did call. I created a handy helper class for my last project:

You can use it in the processor or for GUI as well (I use it for my XY-dragger):

for each value you create one of these, and you set the lambda to do whatever you wanted in the parameterChanged(). It also stores tha value in an atomic variable, making it thread safe:

// member like this:
ParameterAttachment frequency { apvts };
ParameterAttachment gain { apvts };

// in constructor:
frequency.attachToParameter ("frequency");
frequency.onParameterChanged = [&] { updateFilter(); };  // or whatever is apropriate in your case
gain.attachToParameter ("gain");
gain.onParameterChanged = [&] { updateAnotherFilter(); };

// safe to use in processBlock()
auto freq = frequency.getValue();  // calls load() on the atomic

There are more solutions, but maybe that gives you inspiration

3 Likes

Hello Daniel,
great thanks for your support. I will consider your proposition, it look quite interesting.

I also think that maybe I will stay with the official solution, like in the subject. But instead comparing whole parameters id strings, I will provide each parameter’s ID unique first character. Then I will compare only first char, so I will also be able to use switch instead of else if.

By the way I have also other issue. I wonder is there any way to set my parameter to be saveable like normal parameter, but to be invisible for automations?

I mean I have some GUI parameters like zooming, changing visibility of components, and I want it to be saveable, but I want to avoid automation for those parameters.
Also I wonder about “saveable” but only something like last state - remembering last opened GUI settings, but want to avoid saving it in presets with other sound parameters.

Well think about how SlideAttachment is working (or look at the source to 100% confirm) it’s going to be doing something like what Daniel has shown, and it makes a lot more sense than having unique 1st character to your parameter IDs.

You can save whatever you like in getStateInformation, how you achieve that is up to you :slight_smile:

The way I do it for simple things like zoom, I just add attributes to the APVTS XML, eg.
in getStateInformation I do stuff like:

    std::unique_ptr<XmlElement> xml (apvts.state.createXml());
    xml->setAttribute(Identifier("zoom"), zoomSetting);
    copyXmlToBinary (*xml, destData);

and in setStateInformation I do:

    std::unique_ptr<XmlElement> xmlState (getXmlFromBinary (data, sizeInBytes));

    if (xmlState != nullptr)
    {
        auto scale = xmlState->getDoubleAttribute("zoom", 0);
        if(scale == 0)
        {
            scale = 1.0f;
        }
        zoomSetting = scale;

        if (xmlState->hasTagName (apvts.state.getType()))
        {
            apvts.state = ValueTree::fromXml (*xmlState);
        }
    }

for something more complex I tend to keep the state in some other ValueTree and then you can add that tree into the APVTS in getStateInformation, eg:

    std::unique_ptr<XmlElement> stateXml (someValueTree.createXml());
    xml->addChildElement(stateXml.release());

and then in setStateInformation you can extract that and remove the node (this is important!) before setting the APVTS state, eg.

    std::unique_ptr<XmlElement> xmlState (getXmlFromBinary (data, sizeInBytes));

    if (xmlState != nullptr)
    {
        auto someOtherStateXML = xmlState->getChildByName(theIdentifierUsedToInstantiateSomeValueTree.toString());
        auto someOtherState = ValueTree::fromXml(*someOtherStateXML);
        xmlState->removeChildElement(xmlState->getChildByName(theIdentifierUsedToInstantiateSomeValueTree.toString()), true);
...etc...

you can then copy someOtherState into your other ValueTree.

1 Like

Hello, great thanks for all your support.
it’s true that my way with unique first character of String ParameterID was very painfull.

So you inspired me to write other solution (similar to what Daniel showed). But I made it much simpler (I mean not better but less code lines). But I am not sure if it’s secure. I didn’t use any security to check if pointers aren’t null and no jassert.

And actually it’s a shame, but I still not sure how to use parameterGestureChanged. As you can see it’s empty method in my template. But template works, so what’s the problem. But I suppose there is some important reason they made it pure virtual method.

And also I didn’t implemented any AsyncUpdater, I am not sure if it’s important. At the moment it looks like everything works fine. But probably I don’t see bigger picture.

So I am not sure if it’s good solution. If you could give me some feedback I would be very appretiate.

This is my class template:

template<typename ValueType>
class ParameterAttachment : public AudioProcessorParameter::Listener
{
public:
    ParameterAttachment(RangedAudioParameter *rap, std::function<void(ValueType)> func) : rangedAudioParameter(rap), onParameterChanged(func)
    {
        rangedAudioParameter->addListener(this);
    };
    
private:
    void parameterValueChanged(int parameterIndex, float newValue) override
    {
        onParameterChanged(rangedAudioParameter->getNormalisableRange().convertFrom0to1(rangedAudioParameter->getValue()));
    }
    
    void parameterGestureChanged(int parameterIndex, bool gestureIsStarting) override
    {
        
    }
    
    RangedAudioParameter* rangedAudioParameter = nullptr;
    std::function<void(ValueType)> onParameterChanged;
    
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ParameterAttachment)
};

To use it you need only two lines of code. First declaration in processor class body, like:
ParameterAttachment<const float&> inputGainAttachment;

And then in inicialisation list of AudioProcessor, like that:

inputGainAttachment(myAPVTS.getParameter(INPUT_GAIN_ID),
                    [&] (const float& _input) { setInputGain (_input);  })

And that’s all.

Probably you will also notice there is no Atomic values which could be used in processBlock, but I provided it inside of all std::function<void(ValueType)>. So I am not affraid about that.

But maybe there are other hazards in my code.