Issue with parameter automation in DAW

I’ve been developing a vst plugin in Juce and I’ve run into an issue which I need some more context on.
I’ve noticed that if I use my plugin in a DAW everything works great except for automation. Parameters will only automate if I have the plugin window open. If I close the window, the plugin still runs but any automation being sent to it is ignored. I’ve tried this on both Ableton and Cubase with the same results. I’ve also tried it on both Mac and Windows.

I assume that I’m doing something wrong with my implementation of Juce’s parameter system, but the issue is so specific I’m not really sure where to starting looking to find the cause of it.

Any thoughts?

Without knowing how you respond to parameter changes, it’s impossible to know. But if you have added your Processor component as a listener for each of the parameters you’re interested in, then those automation changes should end up in your parameterChanged() function, where you can pass them on to your DSP code.

1 Like

Well actually that really clears things up haha. I had no idea I needed to implement a parameterChanged() function. Up until this point I’ve just been creating parameters, adding slider attachments, and reading and writing to the xml value tree state.

As someone else already said you need a parameter layout.
You also need an attachment that updates from the parameter, not the gui.

Juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout();

Then

juce::AudioProcessorValueTreeState apvts{ *this, nullptr, “Parameters”, createParameterLayout() };

These should be public in your processor so you GUI can access them and update them according to slider position.

For actually adding the parameters in your processor *.cpp

juce::AudioProcessorValueTreeState YOUR_CLASS::createParameterLayout(){
juce::AudioProcessorValueTreeState::ParameterLayout layout;

// add your parameters like so
layout.add // an example gain
(std::make_unique<juce::AudioParameterFloat>( 
“ID OF YOUR PARAMETER”, // the id you call for values
“NAME OF YOUR PARAMETER”, // what name will display beside value
juce::NormalisableRange<float>(-30.0f, 30.0f), // range of value
0.0f // default value
 ));

return layout;
}

I’ve been working on changing how my parameters and dsp code interact. Now that I’m aware of the parameterChanged() function it occurred to me that my values were all changing based off UI events like sliderValueChanged and perhaps automation bypasses this because it acts directly on the parameter. And so perhaps when the plugin window isn’t open, the UI events don’t occur.

I considered implementing the parameterChanged() function but didn’t want do end up with a gigantic sequence of ‘if’ conditions. I found the parameter attachment class and implemented that along side my slider attachments. Before this I just had the slider attachments.

    pOscNumber = std::make_unique<ParameterAttachment>(*valueTreeState.getParameter("oscNumber"), [this](float f) { audioProcessor.Oscillator.SetOscNumber(f); });
    vOscNumber = std::make_unique<AudioProcessorValueTreeState::SliderAttachment>(audioProcessor.vts, "oscNumber", OscNumber.Knob);

Now my dsp code is changed through this callback function which triggers when the parameter is changed. And the slider attachment changes that parameter when the slider is moved. Everything is working just like before and so I tested this with automation.

To my surprise the parameter still won’t automate in my DAW when the plugin window is closed!

So now I’m reevaluating things and trying to figure out what to try next. Is my current understanding of automation acting directly on parameters correct? If so, why doesn’t my parameter attachment work as intended?

Any advice on this would be appreciated.
Thanks.

there are no if statements.

you simply just add the parameters to your layout and update them from a function called GetRawParameterValue(“your_id”)

// set your dsp values like so (in your ProcessBlock)
compressor.setThreshold(*apvts.getRawParameterValue("Threshold"));

// or you can do it like:
auto threshold = *apvts.getRawParameterValue("Threshold");
compressor.setThreshold(threshold);

in your GUI { PluginEditor.h } i put:

juce::Slider sliderThreshold;
juce::ScopedPointer<juce::AudioProcessorValueTreeState::SliderAttachment> attachmentThreshold;

and in your .cpp constructor:

attachmentThreshold = new juce::AudioProcessorValueTreeState::SliderAttachment(audioProcessor.apvts, "Threshold", sliderThreshold);
/*
SliderAttachment
(
    the value tree state from your processor, // [1]
    the id of the parameter this slider is getting, // [2]
    the slider that will effect this parameter // [3]
)
*/

Interesting. So when you say ‘ProcessBlock’ you literally mean to update the values on the audio thread each time you process a new block? It seems like maybe my issue stems from the values not being updated in this way.

So then a Parameter Attachment callback function is not sufficient to keep a dsp value synched with a parameter? That seems strange to me, but if that’s how it is then I can just do the process block method.

And from what you’re saying it looks like a slider attachment on its own is sufficient to get a parameter to respond to automation. You just have to update your values from a process block?

works for me. never had an issue this way. let me know if you’re still having problems

1 Like

First off it is good to use the ParameterAttachments for the GUI updates. It makes no difference if you use the AudioProcessorValueTreeState and the Attachments in there, or if you use the ParameterAttachments that work without the APVTS.
The if branches in the parameterChanged() callback can and should indeed be avoided.

For the issue that it’s not updating, there are some caveats that might make the update seemingly not to work, e.g. a ComboBox, if it has no choices at the time of creating the ComboBoxAttachment will reset the value because the initial update failed (you cannot set the ComboBox to anything else than the 0).

In the audio processing it is indeed advisable also not to use the ParameterAttachment, since they are designed to interact with the GUI thread. I haven’t checked, but some things might introduce asynchronous steps that are only good for GUI updates.

Instead you use the atomic value in the parameter. One way is to use the getRawParameter("paramId") as described above (but please keep the returned atomic<float>* param as member to avoid any overhead.
The better way (IMHO) is to keep a pointer to the actual parameter (again keep it as member):

// as member
juce::AudioParameterChoice* oscNumber = nullptr;

// in constructor
oscNumber = dynamic_cast<juce::AudioParameterChoice*>(getParameter ("paramId"));
jassert (oscNumber); // if this fails there is no parameter with this parameter ID of type choice

// use it
auto currentOsc = oscNumber->getIndex();  // is the correct type, no lookup and thread safe

Hope that helps

1 Like

Automation is working now that I’ve added the updates to the process block.
This is all great information. The distinction about the various attachments being intended only for GUI really clarifies things.

I like the idea of using pointers to the parameters instead of looking them up each time. I’ll definitely be doing that as I have a lot of parameters. Adding all the parameter look ups to the process block felt a little sketchy haha.

Thanks for all the help. I have a much better understanding of the parameter system now.

Quick question for Daniel. I’m implementing the parameter pointers and I’ve run into an issue. I’m not familiar with dynamic casts but it looks like since vts.getParameter() only returns a RangedAudioParameter, I can’t use an AudioParameterInt* or AudioParameterFloat*. If I do this, parameter->getValue() becomes inaccessible. Is there a way to get this cast to work properly so I can use getValue()?

RangedAudioParameter.getValue() returns a 0 to 1 value which I then have to multiply by a scaling value to get the actual parameter value. This adds an extra multiply which kind of throws a wrench in the whole optimization thing. I’d really like to be able to get the scaled value rather than adding additional operations. The nice thing about just using getRawParameterValue() is that it returns that scaled value.

That is exactly what the dynamic_cast is for. As you know you can call methods from the base class just normally, because it is known at compile time. But you cannot know if the pointer actually points to a derived object e.g. of type AudioParameterInt.
That’s where dynamic_cast comes into play. The dynamic_cast will try to convert the pointer to the given type, and if it wasn’t actually of that type it will return a nullptr.

oscNumber = dynamic_cast<juce::AudioParameterChoice*>(getParameter ("paramId"));
jassert (oscNumber); // if this fails there is no parameter with this parameter ID of type choice

The jassert is as an precaution. You know what type the parameter was when you created it. The system will not change it during the whole lifetime, so it is safe to keep that raw pointer.
Only if the parameters are changed e.g. in a refactor two things can happen: someone changed the paramID or removed the parameter alltogether. In this case you get a nullptr.
Or the type could be changed, in which case the dynamic_cast will return a nullptr as well.
To catch those problems as early as possible I usually add a jassert to make sure the pointer was retrieved correctly.

Btw. you can use anything as type in the dynamic_cast, juce::AudioParameterInt* or juce::AudioParameterFloat*. You just need to adapt the pointer type where you are assigning to as well.

2 Likes