Oh, you mean like pushing variable updates only. I considered something similar at some point -buffering the whole state and updating with a pointer swap. I think that only works if there are no interdependencies -if all you have to do is update variables. Any calculations or logic would have to be moved before the push, to parameterChanged, but this can be called concurrently, so you can’t really read the state there -you can’t rely on its coherence. I couldn’t find a way to solve interdependent updates without locking, so I moved all calculations and logic to the audio thread.
Yeah, I think I’ll be OK, all my interdependent things can easily be be done in parameterChange, all I need now is a lock free FIFO - and ‘AbstractFifo’ will be a great start for testing, thanks Juce!
And thanks for bumping this thread, leigh
Ha, you’re welcome @DaveH… thanks for not minding me bumping a year+ old thread.
Thinking through the FIFO approach a bit more, I was wondering about this question that @xenakios brought up above…
Point being, if you’re just sequentially blasting through a whole history of parameter changes at the top of the processBlock, then that could be a whole lot of extra computational load with no benefit. E.g. why recompute filter coefficients 10 times, when only the most recent one will affect the processing?
An alternate approach might be a
StringArray parametersChanged that just holds a list of parameters that have been changed since the last time processBlock was run. Within the
parameterChanged callback, use the
StringArray::addIfNotAlreadyThere method to ensure that each member of the array is unique. Then when you iterate through
parametersChanged at the top of processBlock, fetch the most recent parameter values using the
Performance-wise, I’d guess that calling
StringArray::addIfNotAlreadyThere is going to be slower than just pushing a new message onto a FIFO. However, that would be balanced out by reducing the number of internal recalculations. Exactly which one is more efficient would depend on the particulars of those recalculations, and also on the runtime conditions (running in a session with heavy automation data, and with a Processor whose internals were expensive to recalculate, could really bog down the FIFO approach I would think).
I understand it could be heavy, but how often does the user change every single parameter by hand? Currently I have a limit to the number processed each frame, say 50, which is handy on a program change. But it really is lightning fast, just to grab the values and store them. The process block has the potential of being called thousands of times a second, and as the coefficients are already calculated then there is no danger of overloading the process thread. I’ve used my own FIFO as Juce’s seemed a little over the top and generalised.
I think Xenakios meant why not using the raw parameter values. That’s perfectly ok if they are used directly. The fifo approach makes sense when there are many parameters and most are used in derived forms. Interdependencies complicate it more. For example, I have some delay time set like
outDelayCtl.delay = ftoi_round (outGainLookahead() + levelDetector.peakTime() * std::abs (outGain.mode()));
There are five parameters involved: out gain mode, out gain lookahead, level detection attack, level detection release, level detection compensation. A change in the last one triggers a recomputation of peak time. A change in the previous two triggers it only if the last one is true. Order is relevant: I can’t freely move a mode change around an attack change. This logic has to be computed in a single thread, or in locked threads.
A string array would be a kind of fifo. It would have to be thread-safe. I think having something like addIfNotAlreadyThere in a thread-safe container would be more expensive than dealing with multiple updates. A saner approach could be cleaning while popping -you empty the queue and push to another container with addIfNotAlreadyThere, all in the audio thread. I think it’s too much bureaucracy for the eventuality of multiple updates. I’ve not checked this, but if automation is passed right before processBlock, it seems likely that there wouldn’t be more than one change per parameter anyway.
It’s just way of getting around the fact that parameters can be changed during the block process - which could potentially cause trouble, and you’ll have to use a lock if you’re updating values that interact with each other. This way you don’t, as they are all updated outside the processing loop. The FIFO has no concept of what is changing, it’s just a list of memory locations and values to write to them. It’s a delayed store, in a way.
I’ll continue using this idea, and if it breaks horribly, I have no problem in letting this thread know.
Interesting thread! I’m updating parameters once per block by copying all values from apvts (using getRawParameterValue) to a float array. KISS principle, nothing fancy, but works fine for me.
I am doing the same like @gustav-scholda. Additionally I keep the last value for two reasons:
- I can avoid updating when expensive calls are needed (e.g. creating coefficients)
- Knowing the last value allows to do parameter smoothing (e.g. applyGainWithRamp) for instances where I don’t want a SmoothedValue.
@ gustav-scholda Grabbing and copy all the raw values every block? That can get expensive, especially when the block size is only 1 sample long, like sometimes on FL Studio.
But it may be a good option for just a few parameters -MPSC queues aren’t free either. As a (rather quack but smarter than expected) teacher told us once, the first solution to consider is always statu quo.
Precisely. Didn’t say don’t use the getRawParameterValue. The FIFO idea is not a ‘catch all’, it just gives me peace of mind for the majority of parameters.
I don’t think copying an array is so expensive, and it really frees my mind to think about the important stuff. It’s a matter of taste, but all sorts of smoothing, coefficient calculation on a seperate thread etc. happen inside my dsp classes. This way I can easily import them into another project, define the parameters it needs, and just update them every block without worrying.
I’m more concerned about modularity in the first place. Later on, when the product is roughly finished or at least the parameters are fixed, I start to think about optimization.
@daniel yes, and also this turns out to be really easy because you just need another float array which keeps track of the previous values.
I’ve never been so sure about optimization as a last concern though. Sometimes a design makes an optimization very difficult to make. For example, something I don’t like about the dsp classes is how they couple parameter manipulation with the audio flow. Coming from environments like Max and Reaktor, I miss the distinction between audio and control. So I make my dsp objects use fully derived parameters, and I have different classes for parameter management. A single parameter object can be used by many dsp units, and the whole dsp core can be replaced without recomputations.
Interesting, where are you deriving your parameters then? On a seperate thread? Or once per block as well?
Just once per block. It’s not a distinction in time, but in structure. For example, a trapezoidal integrator is just
y = g * x + s; s = g * x + y; return y;, a 1-pole lowpass is
return integrator(x - integrator.s, g);. “g” may be derived from frequency or from time -that’s not really part of the audio flow. I keep parameter management and proper dsp in different namespaces, so I have say two classes params::LevelDetector and dsp::LevelDetector, with the latter referencing the former. Where many dsp units use the same parameters, they all refer to the same object. Also, because I have the whole dsp core templated for mono / stereo, I can replace it on channel set changes without recomputing parameters.
Thanks for the insight, seems like a nice design!
Btw is it safe to assume that the channel layout doesn’t change between calls to prepareToPlay?
For VST3, the channel layout won’t be changed while processBlock is being called (prepareToPlay is called from initialize / setupProcessing / setActive, after
setProcessing (true) there are no calls to setBusArrangement). Not sure about the rest, but in case of mismatch I don’t process at all (it calls an empty overload).