I’m using an XML specifications file that allows my plug-ins to dynamically infer an AudioProcessorValueTreeState::ParameterLayout for the processor, plus create a GUI in the editor, and also automatically make any attachments between Components and parameters. This works like a charm for plug-in parameters (exposed to the host).
But I’d like this flexible mechanism to also support “realtime properties”. They would be properties that belong to the processor. The processor could read/write them (from processBlock()). But the editor could only read them. And would not be visible to the host (they also don’t need to be stored/recalled).
Of course, I can achieve this manually (like using atomic variables in my processor and read them by polling in my editor), but my wish is to have something with the same benefits of an APVTS: named parameters, lambdas for string<>value conversion, etc.
Frankly, I though about using another APVTS but they have to be tied to a processor, and I’m less than sure about thread safety concerns…
I am in the midst of porting a project into JUCE, that used a proprietary library combined with a much earlier version of JUCE. For parameter management, this proprietary library had a group of classes that defined Parameters, a ParameterListener, a non-blocking, non-allocating ParameterQueue, etc… Each Parameter had a isAutomatable bool that controlled whether it would be visible to the host and automatable, or not, so user-controllable parameters were mixed right in with “realtime properties.” It was interesting to see a totally different approach to parameter management.
So in porting this project into modern JUCE, I am in a similar spot to what you describe. I’ve moved the user-controllable parameters to APVTS management, and then I’ve given the Processor a handful of atomics for exposing realtime properties for the Editor to poll. But it does feel a bit ad-hoc, and I’ve wondered about ways to consolidate organizing those properties (including, as you mention, using a second APVTS – but for the same reasons you mention, decided that wasn’t a great idea).
At the moment, I don’t have an great advice on a better solution, but I’m here for the conversation about it.
For the non user-controllable parameters, have you thought about just adding them as properties to the APVTS state member? You’d have to enforce the read-only editor access yourself, but that would achieve some amount of consolidation.
Also, they’d be stored along with the processor’s state, which is not needed (well, at least in my case).
Today I wrote a very simple RealtimePropertySet class which encapsulates a std::map<String, std::atomic<float>>. The processor (which owns it) can emplace properties from my XML file. I tried to update some values from processBlock() and was able to poll from the editor.
The editor also has an OwnedArray<RealtimePropertySliderAttachment> where RealtimePropertySliderAttachment is just a simple struct to bind a Slider* to the property ID.
This is just a naive test but I’ll keep going to see where this goes.
Most importantly, this work is heavily inspired by the Jukebox SDK from Reason Studios, which is now publicly available.
Right, storing these values is not needed. The use case I often hear for AudioProcessorValueTreeState::state is to use it store GUI window sizing/positioning – i.e. something you do want stored, but also something you don’t want to allow host automation access to.
I’m curious how you are handling access to the setter/getter methods of this class. As you stated above, you want the Processor to write these values, and the Editor to only read them, right?
Also, over in this thread, there has been discussion about not reading/writing atomics per-sample, and instead limiting access to atomics as a per-block operation, for performance/optimization reasons. So even if you encapsulated all the atomics within a RealtimePropertySet, you might still need a collection of floats to temporarily hold these values before writing them to the atomics at the end of the processBlock. (And those should in many cases be floats created “locally” on the stack, within the processBlock scope.) Just pointing out that attempts to keep all the variables needed for this idea in one tidy place starts getting a bit messy.
I’m curious, which part of the Jukebox SDK is inspiring this? I haven’t looked at that Reason stuff in years.
So, yes, I’m using a local variable but that’s not a problem to me. What I do care about is the editor’s ability to be self constructed and updated without a single line of code. So far so good: I do not even have a PluginEditor compilation unit.
Regarding the read/write access, both APVTS and RTPS are protected members of the processor class, which in turn provides a getRealtimePropertyValue (const String paramID) proxy method for the editor.
The Jukebox SDK part inspiring this is inside the Motherboard Object Model where you define custom_properties with specific ownership (GUI, Document, RTC, RT).
OK. Sounds pretty slick, having a self-constructing Editor!
One other thought would be to use an Identifier instead of a String for the property names. It could make the lookup faster when searching for a property in the std::map (I don’t know without testing though), and it would eliminate parameter name typos when coding (compiler wouldn’t complain about a string literal "imput_meter", but it would let you know that propertyIDs::imput_meter is not valid).
You’re absolutely right and that’s what I tested first, but for some reason (which I can’t even remember), I got errors trying doing so, especially when trying to create an Identifier from a property name extracted from my XML spec file. Probably something stupid I missed. I’ll have a look again.
To circle back around to this - I tried an implementation of a RealtimePropertySet today, using a std::map<juce::Identifier, std::atomic<float>> for its data structure. It worked great. So FYI there doesn’t seem to be an issue with using an Identifer instead of a String, as far as the map itself goes.
I’m declaring the Identifiers within a namespace at the top of the Processor header file. So maybe the errors you were getting were in that XML translation step you mentioned.
I guess I’m not able to use Identifiers because property names are inferred from XML at runtime and are not known in advance. Both my AudioProcessor’s APVTS and the new RTPS are build dynamically. And the Editor also uses XML to automatically update by polling the Components that are attached to Real-time Properties.
So maybe I missed something obvious (didn’t take the time to look at it again, to be honest) but I’d think Identifiers would be hard to generate from a dynamic string, right?
But even if that’s the case, I could at least use StringRefs, I suppose.
I guess there’s the issue of putting the Identifiers in a scope that keeps them persistent (they are reference-counted)… like have your Processor own a std::vector<Identifier> realtimePropertyIDs to hold them?
However, maybe the bigger issue is how to USE the Identifiers from your Editor, if the Editor is all dynamically generated. If the Processor’s proxy getter method becomes getRealtimePropertyValue (const Identifier& paramID), and you have to create that Identifier on every call from a String, then that might be a performance drag.
I’m not actually clear on whether “recreating” an Identifier that already exists is slow or fast, in the terms described in the Identifier documentation:
Comparing two Identifier objects is very fast (an O(1) operation), but creating them can be slower than just using a String directly, so the optimal way to use them is to keep some static Identifier objects for the things you use often.
I’ll have to dig through that JUCE code later to figure it out.
To circle back to this question, I got to run some tests today with Juce’s PerformanceCounter, and found there was not a performance improvement in using an Identifier instead of a String for the key of a std::map. They were about the same, with the Identifier tests running about 1% slower on average.
Specifically, I was testing accessing values in the map, not writing to them. And it was a small number of members (8). Perhaps with a larger number of members there would be a speed advantage to using Identifiers.