Inconsistent default values of Sliders in AudioParameterFloat

Hi all,
I am getting a strange behaviour when using AudioParameterFloat with a value tree state: the values actually passed from the slider to my variables are not the one set in the default. Here my code snippets:

auto SystemDecayFilter1Param = std::make_unique<AudioParameterFloat> ("system_decay_panel_filter1",// parameterID
                                                                  "System Decay Filter1",             // parameter name
                                                                  0.9999f,                              // minimum value
                                                                  0.999999f,                         // maximum value
                                                                  0.9965f);                            // default value
    
	

With the debugger placed in updateParameters() of PluginProcessor.cpp I see that the actual value is 0.999899983, which is not the one I set as default (i.e., 0.9965). Why is this happening? I need exact values of my parameters.

void FilterSynthesizerAudioProcessor::updateParameters()
{
    float systemDecayFilter1 = *systemDecayParameterPanelFilter1;    
    fullChain.get<Filter1Index>().get<0>().processor.setSystemDecay (systemDecayFilter1);
}

Moreover, if I change the minimum value of AudioParameterFloat from 0.9999f to 0.0f I get with the debugger that the actual value is now 0.999998986, yet different from the default value I set (and also from the previous value using the previous range).

This is an inconsistent behaviour of the default value and I can’t understand how to make Juce to have the right value from my slider set default value.

you’re setting a default lower than the minimum. that’s not gonna happen. otoh, that 0.999998986 seems to be your maximum -it’s the actual float value for 0.999999.


floating point has limited precision, and your range is too tiny. you may need to use a wider range for the parameter and then scale it in the process, maybe converting to double. otherwise you’re working on very few bits and scalings will turn them to noise.

So, in PluginProcessor.h I have set the variable const float scalingFactor = 10000.0f;. Then I called:

auto SystemDecayFilter1Param = std::make_unique<AudioParameterFloat> ("system_decay_panel_filter1",// parameterID
                                                                  "System Decay Filter1",             // parameter name
                                                                  0.0f * scalingFactor,                              // minimum value
                                                                  0.999999f * scalingFactor,                         // maximum value
                                                                  0.9965f * scalingFactor);                            // default value
    
	

And then in my updateParameters() function I have scaled down:

void FilterSynthesizerAudioProcessor::updateParameters()
{
    float systemDecayFilter1 = *systemDecayParameterPanelFilter1 / scalingFactor;    
    fullChain.get<Filter1Index>().get<0>().processor.setSystemDecay (systemDecayFilter1);
}

However, this does not work. The compiler complains with JUCE Message Thread (1): EXC_BREAKPOINT (code=EXC_I386_BPT, subcode=0x0)
What is the right approach to solve the issue?

if you’re using from 0 to 0.999999, i’d make the parameter go from 0 to 1 and limit it in the process. scaling with such a small difference seems like a bad idea. if you’re using a small range like 0.9999-0.999999, i’d also make the parameter go from 0 to 1, or 0 to 100, and then scale in the process -say, setting a member variable for each parameter change. i don’t know your use case, but a small range like that is weird from a user’s perspective anyway -i’d hide it in the implementation.

Hi @kamedin, actually I don’t understand what you meand with “scale in the process”. Isn’t it what I did in my second post?

i mean let the parameter go from 0 to 1, and scale it when you get it (in a listener callback, or calling getValue).

My method worked. The issue was that the constructor was not called before the use of that variable. Indeed I am using the value tree and that is called first so the variable is not initialized.

FilterSynthesizerAudioProcessor::FilterSynthesizerAudioProcessor()
     : AudioProcessor (BusesProperties().withOutput ("Output", AudioChannelSet::stereo(), true)),
       parametersPanelFilter1 (*this, nullptr, Identifier ("ParametersPanelFilter1"), createParameterLayoutPanelFilter1())
{

    systemDecayParameterPanelFilter1 = parametersPanelFilter1.getRawParameterValue ("system_decay_panel_filter1");
}
	
AudioProcessorValueTreeState::ParameterLayout FilterSynthesizerAudioProcessor::createParameterLayoutPanelFilter1()
{

	auto SystemDecayFilter1Param = std::make_unique<AudioParameterFloat> ("system_decay_panel_filter1",// parameterID
                                                                  	"System Decay Filter1",             // parameter name
                                                                  	0.0f * scalingFactor,                              // minimum value
                                                                  	0.999999f * scalingFactor,                         // maximum value
                                                                  	0.9965f * scalingFactor);                            // default value

To avoid this issue I created a global variable const float scalingFactor = 10000.0f;

Now I have another issue. I would like that the label associated to my slider displays the scaled value, not the value of the slider. I can’t find a function that allows me to do this. I am using this code for handling the slider and associated label in my gui editor subcomponent:

class PanelFilter 
{
public:
    
    PanelFilter (AudioProcessorValueTreeState& valueTreeState)
    {
        filterSystemDecayLabel.setText ("System Decay", dontSendNotification);
        filterSystemDecayLabel.attachToComponent (&filterSystemDecaySlider, true);
        addAndMakeVisible (filterSystemDecayLabel);
        addAndMakeVisible (filterSystemDecaySlider);
        filterSystemDecayAttachment.reset (new SliderAttachment (valueTreeState, "system_decay_panel_filter", filterSystemDecaySlider));
		
	}	

I think that is a crutch. The accuracy of float is not determined by the number of decimals after the comma, it is a binary expression in an scientific notation like number. Moving the comma just changes the exponent, and since you don’t do that in binary domain, you are even adding more conversion artifacts.

Ok… then what is your suggestion?

Ok, when reading again, I realised it might make sense. BUT when you specify the values, you should use double for your scaling factor, otherwise you still have the artifacts. Or just fill in the scaled numbers, so you don’t get conversion warnings, if you have turned them on.

And specify the NormalisedRange like I wrote in your other thread. You didn’t supply a step range, so the numbers are allowed to be somewhere between the nicely readable ones.

How about that for example:

auto SystemDecayFilter1Param = std::make_unique<AudioParameterFloat> 
    ("system_decay_panel_filter1",         // parameterID
     "System Decay Filter1",               // parameter name
     NormalisableRange<float>(0f,          // minimum
                              0.999999f,   // maximum
                              0.000001f),  // step size
     0.9965f,                              // default value
     String(),                             // label
     AudioProcessorParameter::genericParameter,
     [](float value, int maximumStringLength)
     {
         return String (value * 0.00001);  // double is intended!
     },
     [](const String &text)
     {
         return float (text.getDoubleValue() * 0.00001); // explicit cast to avoid warning
     }
);

However, I was wondering how user friendly a parameter is, that is changing 6 digits after the comma? Maybe re-think, what the value is actually representing?