Hey Y’all! Another question about the APVTS. I’m trying to determine what the best/most correct way to handle parameter changes within my plugin processor and its members. Obviously theres the two broad methods of either:
a) Store a reference or pointer to the parameter and read its value whenever you need it.
b) Deal with listeners and callbacks.
Then if you go down the listener route, you have to decide whether to:
a) Add a listener through APVTS
b) Add a listener directly to the parameter
c) Add a listener directly to the APVTS value tree.
I’m a little confused because while there are some obvious differences, I don’t have enough experience to determine which ones the best or most correct to use. Also there’s the matter of thread safety, which I don’t know much about either.
What’s the standard way for reading and responding to parameter changes in the processor? And if there’s more than one standard way, what are the advantages of each?
people have been asking the juce team to add support for sample accurate parameters for a long time. they will make this question more straight forward to answer, but for now there’s max 1 parameter value per block, which is why most people just load them in processBlock as far as i know. this is not a straightforward answer because there’re also parameter listeners for some reason and it’s not clear what the advantage of those could be
I would avoid the parameter listener, because it is undetermined on which thread it will appear (it is called synchronously, so it can come from the GUI or the audio thread automation). This means you have to deal with thread safety yourself.
I second Florian’s version of reading the parameters you need at the beginning of processBlock. If you have heavy operations, you can store the previous value and check yourself, if the value has changed.
In many cases you need some form of parameter smoothing anyway, because sudden jumps in parameters can lead to artefacts, where it is nice if the plugin prevents them.
Which is ok for simple plugins but not for plugins with hundreds of parameters. It’s easy enough though to listen and add dirty parameters to a lock free queue for subsequent processing on your preferred thread.
It’s worth remembering that most of the time most of the parameters aren’t going to change. Perhaps the one the user is tweaking and a few host automations.
i think sample accurate parameters will not lead to people load a unique parameter value for each sample index, especially not on most or all parameters of a plugin, since it isn’t even sure if DAWs even send sample accurate parameter changes. some might just update every 64 samples or something and it results in something steppy anyway. but at least it lets people make their own sub-block processing for each block and then each sub block could start by getting the parameter values, rather than the whole block, and the parameter smoothing wouldn’t have to work so hard, could probably get away with shorter smoothing times sometimes, which is nice for certain transitional automation situations.
edit: and this is straight forward because it makes it 100% clear that vague paramter listeners are not an option anymore
Thanks for all the advice. I’m liking the pattern of reading the parameter values at the start of the process block. However, for situations where parameter values need to be processed into something else before being used in the process block, I’m finding APVTS listeners very helpful. I’ve got a pattern of creating a generic helper class where I can provide an APVTS reference, a list of parameter IDs, and a callback, and use that to set a ‘shouldRecalculate’ boolean to true whenever the parameters change. This saves having to recheck every value every time.
even with a modulation system i’d read the parameter values in processBlock and then do the modulation stuff there as well. cause you might run into the situation that you wanna add a modulator that depends on information that only exists in processBlock, like project position for temposync lfos, midi inputs for keytracking or even the audio signal for envelope followers
Each parameter I create has its atomic pointer, so I don’t worry about updating. However, it does not seem convenient to me nor would I know how to deal with changes during block processing since process times have nothing to do with changes in real time. So at the beginning of each block I store the value. In case interpolation is required, I save that value at the end.
float newVolume = *params.volume;
outputBuffer.addFromWithRamp(0, startSample, inputBuffer, numSamples, oldVolume, newVolume);
oldVolume = newVolume;