OoP: Standardization between AudioProcessorParameter, GUI, target object(s) and Host?


#1

Hey all,

I’m a student still developing my mind in the wonderful world of OoP. I’m trying to figure out the best way to go about designing my subclasses for a project I’m working on at my internship.

I’m trying to wrap my head around the wisest way to go about making everything communicate properly. A subset of the project I’m working on is my ‘MultiOscSynth’- a subclass of Synthesiser. For simplicity’s sake, let’s say it has 3 user-controllable attributes- level, numberOfLayers, and coarseTuneInSemitones.

These user-controllable attributes should be changeable by the GUI or automation. Whenever the attribute is changed by anything, the following should happen:
• a parameter object representative of this user-controllable attribute should be updated
• the target object (MultiOscSynth) should be notified to handle the change with its local attributes
• the corresponding GUI element should reflect the new value (which does not happen by default with automation changes)
• The host (DAW) is notified of the change so that it may update automation lanes etc.

I created an AudioProcessorParameter ‘ISWParameter’ to address the first bit; an ISWParameter is any user controllable parameter. In addition to some meta data, an ISWParameter maintains pointer(s) to ISWObject(s)- a class made for the sake of polymorphism, whose subclasses must override/define the method “updateFromParameter(ISWParameter* paramToUpdateFrom)”. MultiOscSynth is one of these subclasses. Whenever setValue is called on an ISWParameter, it tells this ISWObject to updateFromParameter.

Additionally, I made an ISWParameterFollower class that maintains a pointer to an ISWParameter; this is useful for GUI elements to ‘know’ what parameter they control. My ISWSlider class, per example, is a Slider, ISWObject, and ISWParameterFollower- it’s an ISWObject so that an ISWParameter can maintain a pointer to it and tell it to update, and an ISWParameterFollower so that whenever my Slider::Listener detects a GUI change, it can generically tell the appropriate ISWParameter to update its value (rather than having a long list of conditionals per GUI element).

Question 1: Are these object relationships appropriate? Is the coupling between a GUI element and a parameter an issue? (ISWParameter knows of a corresponding GUI element ISWObject to update; a GUI element like ISWSlider knows of a corresponding ISWParameter to update).

The rest of my challenge is figuring out proper standardization/normalization between all objects responding to a parameter change.

‘level’ is the easy case. the host/DAW expects it to be 0->1, ISWParameter (an AudioProcessorParameter) expects it to be 0->1, the GUI Slider goes from 0->1, and the local attribute in MultiOscSynth goes from 0->1.

‘numberOfLayers’ is a bit more of a challenge. The associated GUI rotary Slider is to move in quantized integer steps from 1 to 7. multiOscSynth also wants to interpret values this way. However, the host/DAW and the ISWParameter still want this between 0->1.
The obvious solution that comes to mind is to store these values as reciprocals, and to perhaps have a boolean in ISWParameter denoting it as storing reciprocals.

‘coarseTuneInSemitones’ presents a second issue- negative values. The associated GUI element will go in quantized integer steps from -12 to 12, per example, which again multiOscSynth wants, but the host + ISWParameter do not. To address this, 0 could be mapped to 0.5 with negative values occupying 0->0.5 and positive values 0.5->1, with the reciprocal / 2.0 providing the displacement from 0.5. Still, this is a special case and I’m not sure where it should be addressed- if the ISWParameter should maintain a boolean denoting it as this case and handle values accordingly, or what.

There are other possible cases that could occur. For example, in designing a lowpass filter with a ‘cutoffFrequency’ the associated GUI element might go from 20->20000 (hz)… storing a scalar in ISWParameter (in this case, 1/20000) could address it.

Question 2: Should all value conversions/standardization occur within an AudioProcessorParameter? Is addressing all special cases with flags/conditionals necessary? Does JUCE provide a better means of standardizing values between objects?

Thank you for your advice!


#2

If you take an AudioProcessorValueTreeState as your model, most of your structures are already present.

Check out the Attachment classes, e.g.:
AudioProcessorValueTreeState::SliderAttachment

The conversion of 0…1 to the NormalisableRange is done by createParameter. If you use a AudioProcessorValueTreeState::Listener the value in the callback is already adapted to the range. But if you call getParameter() and getValue(), the value is between 0…1 (because this method is called by the host).

If you use these callbacks only for getters and setters, you should be fine in terms of multi threading (setting a float is atomic afaik).

About polymorphism: the AudioProcessorValueTreeState::Parameter inherits the AudioProcessorParameter and has spezialisations for different value types already…

Hope that helps


#3

Thank you! I’ll try reading through this documentation, though admittedly I sometimes have a hard time understanding things without examples.

Let’s take the coarseTuneInSemitones case to see if my understanding is correct. In the processor, I’d create an ISWParameter (AudioProcessorParameter subclass) representative of that user-controllable parameter. For the rotary Slider itself, I set its range from -12 to 12 with step sizes of one. How do I create a SliderAttachment to connect these two?

Furthermore, when sliderValueChanged is called in my AudioProcessorEditor, what method(s) am I to use to update the parameter’s value? Will the parameter’s value being updated elsewhere (such as by automation) update the appropriate Slider autonomously thanks to SliderAttachment?

Finally, I’m still not understanding where the mapping from my rotary Slider’s range to 0->1 is occurring. How do I ensure this occurs when I call setValue on my ISWParameter?


#4

I wrote a minimal example some weeks ago, see at github:

The range of your slider is already defined when creating the parameter, see:
https://github.com/ffAudio/ffTapeDelay/blob/master/Source/PluginProcessor.cpp#L30

And creating a Slider attachment is enough to set the sliders range, step and set the initial value, see:
https://github.com/ffAudio/ffTapeDelay/blob/master/Source/PluginEditor.cpp#L27

The attachment is a listener of the AudioProcessorValueTreeState, so it transports the value from the host (who called setValue from automation) to the slider and vice versa, being a slider listener and calling setValueNotifyingHost.

The slider attachment takes care of this.
And the other direction works like this:
either you do it yourself by reading the value each time processBlock is called, like I did here:
https://github.com/ffAudio/ffTapeDelay/blob/master/Source/PluginProcessor.cpp#L135

Or you register as AudioProcessorValueTreeState::Listener as written above, in which case the float value is already in the range given at createParameter, see:
AudioProcessorValueTreeState::Listener::parameterChanged ( const String & parameterID, float newValue)


#5

Thank you! I’ll be sure to try this.

My next question, though, is what’d be the wisest way to inform my MultiOscSynth when a parameter updates, given that I’d no longer be using a proprietary parameter object with the meta data / ISWObject pointer. Is the solution to make my MultiOscSynth an AudioProcessorValueTreeState::Listener ?


#6

Yes, I would say so. The benefit is, that it reacts the same no matter if the change was made by a gui click, restoring the state via load or any other update.


#7

It’s working perfectly- thank you!

One more question I have is if there’s a smart way to attach some sort of ‘type’ string to the parameter I create with createAndAddParameter. For example, a parameterID for my parameter is “paramMultiOscGain”, which I have added my instance of MultiOscSynth to as a parameter listener.

However, in the conditionals in my MultiOscSynth object’s parameterChanged method, I’d prefer to just do if(paramType == “gain”) rather than if(parameterID == “multioscgain”) to reduce coupling. Is there a settable attribute of the parameter that’s good for this- is it what I’m supposed to be using parameterName or labelText for in the createAndAddParameter?

I guess my hesitance to use either of those attributes is that I’m uncertain which the host/DAW will use for naming things… I’d prefer the automatable name be displayed in the DAW as ‘Multi-osc Gain’ but for conditionals I’d rather compare to a generic ‘gain’ (as I will have multiple ISWSynth’s and can hopefully stick some parameter change handling behaviors in that abstract class’s parameterChanged rather than repeating them in subclasses).


#8

Though it seems I won’t be able to access any parameter attributes without making my MultiOscSynth aware of the processor’s state… hmn.


#9

I’m not sure if I understood your problem, but have a look at the documentation of AudioProcessorValueTreeState::createParameter(), here you see which attributes are available, and I think it has what you need. The parameterID is not visible to the user, so you can name it to whatever you like as long as it’s unique.

Yes, but I don’t see for what purpose. The parameterID is present in the callback, so you can specialize there for any purpose, because it’s unique.


#10

Let’s say I have a project with multiple MultiOscSynth objects representing sound generation sources to be independently controlled. If all of them are attached as Listener’s of the same state, they won’t be controllable individually- for example, any parameter with the ID ‘multioscgain’ being changed will pass the conditional in each MultiOscSynth’s parameterChanged method.

The workaround I can think of is to name parameters like “gain0multiosc” “gain1multiosc” etc and then to simply check the first four characters for the control type, and check the number against an integer ID stored in a MultiOscSynth that denotes which MultiOscSynth instance it is… but I don’t know if the string operations for these comparisons are real-time safe.


#11

“project with multiple MuultiOscSynth” like a project with many instances of your synthesizer/plugin? Every instance will have it’s own state, so no need for cluttering the names.

The parameterID is set upon creation of the parameter and will not change. I would expect that the String reference in the listener callback references that string instance in the parameter class, so I see no reason, why that should be a problem.
And you compare it in the callbackt to compiled literals, so they don’t change either, I would say it should be fine.

Edit: reading the sources is always a good exercise:


#12

MultiOscSynth isn’t the whole plug-in, but a subset of it.

My overarching plug-in has 3 different types of synthesizers that will each be their own independent objects. ‘ISWSynth’ is an abstract class that is a Synthesiser and a AudioProcessorValueTreeState::Listener. ‘MultiOscSynth’ is an implementation of this class; for this particular plug-in, there will be 1-2 other unique ISWSynth implementations/child classes.

Certain things are consistent across every ISWSynth. For example, every ISWSynth has a gain attribute. It would be nice if the ISWSynth could have its own parameterChanged method definition that handles these general attributes, with subclasses’ parameterChanged methods only handling their specialized attributes. The issue is, if ISWSynth is told to respond to a parameterID like ‘synthgain’, every single ISWSynth in the project will set its gain- which is not what I want.

To the issue I was bringing up- imagine I had a plug-in that contained multiple instances of a MultiOscSynth. There’d be no way of independently controlling their gains if their parameterChanged methods had conditionals responding to a parameterID like ‘multioscgain’- changing the multioscgain parameter’s value would update all MultiOscSynth instances rather than just one.


#13

Ok I understand.
But the AudioProcessor will have only one instance of AudioProcessorValueTreeState and there is only one linear list of parameters. So your plan with name prefixing is the route to go IMHO.
However, you can attach an arbitrary number of Listeners, so that should be doable as you proposed.


#14

For this particular project, I’ll be fine having the conditionals go based on specialized parameterID’s like “multioscgain”. However, I was developing my ISWSynth class and subclasses with the hopes of making them reusable in other projects and allowing for multiple instances of a particular ISWSynth object.

If parameterChanged gave me the actual parameter rather than a parameterID, I’d be able to check its meta-data (such as label) for determining the type of parameter. This way I could still have unique parameterID’s (as needed) but handle the type of parameter in the same way.


#15

Ok, this is a discussion you will need to do with someone of the juce team… Good luck with your project :slight_smile:


#16

Made another thread more generalized on the subject- parameterChanged & handling parameter type