APVTS without attachments, with respect to a TwoValueSlider

Hi all,

I’m trying to figure out the best way to handle slider parameters in my plugin, using AudioProcessorValueTreeState. The SliderAttachment method given in the tutorials seems to work fine, expect when using a TwoValueSlider. One solution given in another thread is create a custom attachment, but this seems a little hacky and beyond my current understanding of JUCE, particular the use of the ControlBase. (see https://forum.juce.com/t/using-the-slider-attachment-class-with-2-value-sliders/18411)

So my question is: without necessarily using a custom attachment, what are some good ways synchronise processor parameters with the TwoValueSlider in the editor?

A good old fashioned Slider::Listener would be my recommendation.

void sliderValueChanged(Slider* slider)
{
    if (slider == &twoValueSlider)
    {
        const auto min = twoValueSlider.getMinValue();
        const auto max = twoValueSlider.getMaxValue();

        minParam->setValueNotifyingHost(min);
        maxParam->setValueNotifyingHost(max);
    }
}

And then inherit from APVTS::Listener to get callbacks when the parameter is changed by the host:

void parameterChanged(const String& id, float newValue)
{
    if (id == "min")
        twoValueSlider.setMinValue(newValue);
    else if (id == "max")
        twoValueSlider.setMaxValue(newValue);
}
1 Like
  • Plus adding a re-entrance flag,
  • plus adding beginChangeGesture() and endGesture(),
  • plus atomic safety measure, because the notification from the parameter occurs most of the time from the audio thread, vs. the slider notification come from the message thread…

There is quite a bit of work involved to do it right.

The above solution will:

  • cock up when an automation is recorded, since it doesn’t know when you touch the slider
  • might go into a feedback spiral
  • has thread safety issues

I created a safe passage into the message thread with this helper class:

You can copy that class and create a ParameterAttachment. It is not specific to a TwoValueSlider, but you can use it like that (untested):

AudioProcessorValueTreeState& treeState;
ParameterAttachment<double> minValue { treeState };
ParameterAttachment<double> maxValue { treeState };
Slider slider {Slider::TwoValueVertical, Slider::NoTextBox};
int lastDraggedThumb = 0;

// in constructor:
slider.onDragStart = [&]
{
    lastDraggedThumb = slider.getThumbBeingDragged();
    if (lastDraggedThumb == 1) minValue.beginGesture();
    else if (lastDraggedThumb == 2) maxValue.beginGesture();
};
slider.onDragEnd = [&]
{
    if (lastDraggedThumb == 1) minValue.endGesture();
    else if (lastDraggedThumb == 2) maxValue.endGesture();
    lastDraggedThumb = 0;
};
slider.onValueChanged = [&]
{
    if (lastDraggedThumb == 1) minValue.setValueNotifyingHost (slider.getMinValue());
    else if (lastDraggedThumb == 2) maxValue.setValueNotifyingHost (slider.getMaxValue());
};
minValue.onParameterChangedAsync = [&] { slider.setValue (minValue.getValue()); };
maxValue.onParameterChangedAsync = [&] { slider.setValue (maxValue.getValue()); };

minValue.attachToParameter (minParamID);
maxValue.attachToParameter (maxParamID);

Granted, this is a lot to copy, makes sense to wrap that into a MinMaxSliderAttachment class (haven’t needed a MinMax Slider so far).

Hope that helps…

4 Likes

Thanks to both @Daniel and @Im_Jimmi for your responses.

I’ve used Daniel’s solution mostly-successfully. There’s a few small errata that I will post for the sake of anyone viewing this thread in the future.

  • slider.onValueChanged -> slider.onValueChange
    
  • slider.setValue -> slider.setMinValue()   for the minimum
    
  •                 -> slider.setMaxValue()   for the maximum
    

I’m not so sure about this change however:

  • [min|max]Value.setValueNotifyingHost() -> [min|max]Value.setNormalisedValue()
    

The ParameterAttachment class that @Daniel provided does not have setValueNotifyingHost(), so setNormalisedValue seemed like the closest option. However, I’m not necessarily passing in normalised values - could this be a problem?

There also appears to be an issue with the mouse tracking here; in certain positions, dragging the thumbs causes them to rapidly oscillate to and from their original position. Do I perhaps need to implement some more onMouse…() functions?

Thanks for your help here @Daniel, I really appreciate it.

Matt.

Good spot, yes I didn’t test the code, so I made those mistakes you pointed out.

Using setNormalisedValue is indeed a problem, you are right, the class needs a setValue for unnormalised values. The logic follows the conversion in getNormalisedValue, just the other way round.

The oscillation effect you observed could be explained by that, since the value is handed to the host and is asynchronously returning, but wrongly not unnormalised. I hope it goes away once that is changed.

@Im_Jimmi sorry, I didn’t mean to sound that harsh, you gave a good starting point. It actually took me a year to realise, that my plugins had those problems with automation I were pointing out.

2 Likes

No problem at all! You comment reminded me to double check my own code for these sorts of things!

1 Like

Thanks David. I’m not sure I understand what you mean by “the other way around”, here is my attempted implementation of setValue():

void setValue (ValueType newValue) {
    if (parameter)
        parameter->setValueNotifyingHost (newValue);
    else
        parameterChanged (paramID, newValue);
}

This still suffers from the same problems. I also tried using …

parameter->getNormalisableRange().convertFrom0to1(newValue)

… in the true branch of the if-statement, but that caused a failed assertion. How do you think setValue() should be properly implemented?

Thanks.

I added it now:

It is not used in my code, so I couldn’t make thorough tests on it. Maybe I should add a two-value Slider to my project, but that’s for later.

Could you let me know, which assert fired? (File and line, and ideally the comment around, if there is any)
Thank you for using it.

1 Like

So I tried your implementation of setValue, and the same assertion was triggered for convertTo0to1() as for convertFrom0to1():

static ValueType clampTo0To1 (ValueType value)
{
    auto clampedValue = jlimit (static_cast<ValueType> (0), static_cast<ValueType> (1), value);

    // If you hit this assertion then either your normalisation function is not working
    // correctly or your input is out of the expected bounds.
    jassert (clampedValue == value);

    return clampedValue;
}

On line 253, juce_NormalisableRange.h

Ok, that jassert makes sure, that your normalisation is legit.
The fact that it fails can have two reasons:

  • the normalisation function of your parameter delivers values outside 0 to 1
  • values outside from the legit unnormalised range are fed into the normalisation
    e.g. the range is 0…10 and you feed 11, so the clampedValue will be different from the value coming in.

Go out the stack step by step and check the numbers to find out what happened.
If all else fails, please share the range, how you set up the parameter.

Oh, and since you are not using the regular SliderAttachment, you will have to set the slider range manually to be the same like your parameter’s one (and min and max parameters ranges need to be identical too).

1 Like

Daniel, thank you so much for your help! Setting the range manually fixed the clamping issues and now the slider behaves perfectly. For any future visitors, here is a working TwoValueSliderAttachment class with the inclusion of a lambda that sets the text value in the “[min] - [max]” format:

class TwoValueSliderAttachment {
public:
    
    ParameterAttachment<float> minValue;
    ParameterAttachment<float> maxValue;
    int lastDraggedThumb = 0;
    
    
    TwoValueSliderAttachment(AudioProcessorValueTreeState& vts, const String minParamID, const String maxParamID, Slider& slider)
    : minValue (vts), maxValue (vts) {
        
        auto minParam = vts.getParameter(minParamID);
        auto maxParam = vts.getParameter(maxParamID);
        
        // Expect minParam == maxParam
        jassert(minParam->getNormalisableRange().start ==               maxParam->getNormalisableRange().start
                && minParam->getNormalisableRange().end
                == maxParam->getNormalisableRange().end);
        
        auto minDefault = minParam->convertFrom0to1(minParam->getDefaultValue());
        auto maxDefault = maxParam->convertFrom0to1(maxParam->getDefaultValue());
        
        slider.setRange(minParam->getNormalisableRange().start, minParam->getNormalisableRange().end);
        slider.setMinAndMaxValues(minDefault, maxDefault);
        
        slider.onDragStart = [&]
        {
            lastDraggedThumb = slider.getThumbBeingDragged();
            if (lastDraggedThumb == 1) minValue.beginGesture();
            else if (lastDraggedThumb == 2) maxValue.beginGesture();
            slider.updateText();
        };
        slider.onDragEnd = [&]
        {
            if (lastDraggedThumb == 1) minValue.endGesture();
            else if (lastDraggedThumb == 2) maxValue.endGesture();
            lastDraggedThumb = 0;
            slider.updateText();
        };
        
        slider.onValueChange = [&]
        {
            if (lastDraggedThumb == 1) minValue.setValue(slider.getMinValue());
            else if (lastDraggedThumb == 2) maxValue.setValue(slider.getMaxValue());
            slider.updateText();
        };
        
        slider.textFromValueFunction = [&] (double input)
        {
            juce::ignoreUnused(input);
            String output = String(slider.getMinValue(), 2);
            output += " - ";
            output += String(slider.getMaxValue(), 2);
            return output;
        };
        
        
        minValue.onParameterChangedAsync = [&] { slider.setMinValue (minValue.getValue()); };
        maxValue.onParameterChangedAsync = [&] { slider.setMaxValue (maxValue.getValue()); };
        
        minValue.attachToParameter (minParamID);
        maxValue.attachToParameter (maxParamID);
        
    }
};

Hopefully this proves helpful. Thanks again Daniel.

2 Likes