AudioProcessorValueTreeState - Possible additions?


#1

Hi Guys,

I’ve recently been thinking a little more about parameters and the new classes etc.

I currently have a derived parameter class of my own which basically takes a lambda in its constructor to be called when the virtual setValue() function is called etc. The class has a few other little additions in there for NormalisableRange stuff etc. With the exception of the lambda it’s pretty darn similar implementation wise to the AudioProcessorValueTreeState::Parameter class.

I also use std::atomic internally for the value being set.

I saw this recent comment from Fabian:

I’ll look into this in more detail later today (need to wait until my FL studio download finishes :slight_smile:). Using AudioProcessorValueTreeState and SlideAttachments etc. is really the idiomatic way to do parameters nowadays and JUCE should really work when using these.

My question is would it be a worth while addition to use std::atomic in these parameter classes based on one of the compiler defines (COMPLILER_SUPPORT_C++11 or whatever) ?

I’m also wondering about the lambda/std::function approach rather than the use of listener/broadcasters.

For example to keep DSP units self contained am I right in thinking you would need to register the Processor as a listener for every parameter (using the AudioProcessorValueTree classes) ? Then in the listener callback you would need to have some kind of if / switch block checking against the ID of the param being changed etc ?

I’m wondering whether this is a little more boiler plate needed than a lambda (and potentially array of lambdas) added as a member function and whether a facility to add a std::function based callback would also be worthwhile in any of these classes alongside the listener/broadcaster based approaches ? (Again based on some compiler support macro)

Would be interested to know the JUCE teams thoughts on the future of the Parameter classes with modern C++ support etc.

Cheers


#2

Just as an aside did a getUnnormalizedValue() ever get any approval?

It’d be a pretty useful addition for updating some GUI elements in some circumstances as getValue() obviously only returns normalized. It is possible to get around this by overriding the getText() function in some cases to return a textual representation of the full range value but getUnnormalizedValue would be super handy.

I gather this might only be applicable to the classes with a NormalisableRange though like AudioProcessorParameterFloat.

It would be very useful as a virtual function though. Particularly when creating component classes similar to the parameter slider in the demo plugin. So that it is possible to refer to the generalise base class AudioProcessorParameter rather than the specialised versions.

EDIT: I can see the SliderAttachment class sort of handles this situation already via the call to state.getParameterRange(). So doing this may be overkill. Still I think the std::atomic and lambda additions could still be worth some thought ?


#3

Just out of interest. Are people handling the lack of explicit atomicity in the parameters (the value member) by just never reading the parameter value directly in another thread ? i.e. in the processBlock ?

So would the preferred approach be to respond to a parameter change (via a listener callback etc.) by always setting the related object/widgets value.

In other words with a parameter like filterCutoff you would avoid calling filterCutoff.getValue() in the processBlock ? Instead you would only ever read the param value directly in the listener callback and then update a filter object or whatever.

Say:

myFilter.setCutoff(state.getParameterRange().convertFrom0To1(filterCutoff.getValue()));

Then only ever read/process values from the myFilter object in the processBlock ?

I’m just interested to hear how people are handling these kind of things.


How are people handling AudioProcessorValueTreeState::LIstener callbacks?
#4

I don’t think this is necessary: technically, according to the c++ standard, you are right: reading a parameter value is not atomic. But practically, on all x86/arm hardware - therefore on all hardware supported by JUCE - reading/writing floats is atomic even without std::atomic as it translates into a simple load/store machine instruction. So simply copying the parameter value to a local float at the beginning of the processBlock is thread-safe.

Yes, I think you are right. JUCE could use more lambda/std::function solutions all round. The problem is that we always need to have this as an optional API as RTAS plug-ins need to be compiled with older compilers. But it’s definitely something we are looking into.

Important Edit: Although this seems to work mostly in practice it is unsafe as compiler optimisers become better and better. Without a std::atomic the optimiser may make the assumption that the value is never modified by an outside thread and therefore you could get unexpected results. The JUCE team is working on adding std::atomic to all the internal parameter handling code. (Edited 9th of October 2017)


#5

Cheers Fabian.

That’s sort of what I had thought in terms of float atomicity. I think I was just being overly pedantic after watching Timurs Cpp Con talks again.

Good to know the lambdas might start creeping in.

Josh


#6

To be fair, making it std::atomic probably wouldn’t hurt as IIRC these boil down to the same instructions as a plain float if plain floats are atomic on the platform?


#7

I guess it’d only be of “real” value (given JUCE’s supported architecture) to explicitly express intent. “Hi this value is gonna get played with by the GUI thread” etc etc…


#8

One concern is whether the read is atomic. Worst case scenario, you get a torn read. You’re right, this is not a concern on current platforms.

Another concern is low level memory ordering. Personally, all my parameter interactions are acquire/release so I can make reasonable assumptions about causality in multithreaded programs. This is automatic on x86, but not on arm! Hence, programs will behave differently.

Lastly, and this is the real problem: Since the C++ compiler deems it undefined behaviour to have data races, it can and will make assumptions about data ownership - for instance, it can spill register values to unrelated memory locations (like a paramater in use, since it’s in the L1 cache anyway) because it concludes no other thread can see this behaviour (the as-if optimzation rule). Here’s a blog that actually shows this behaviour:
https://software.intel.com/en-us/blogs/2013/01/06/benign-data-races-what-could-possibly-go-wrong

In short, I would strongly advice you to fix this behaviour!


Test for concurrency issues PluginProcessor <-> PluginEditor?
#9

@Mayae

Your last point is exactly what I was interested in. The underlying compiler optimisations/interference that might occur without the explicitly expressed concurrency.

Thanks for that link. I will have a good read.

Josh