AudioProcessorValueTreeState Attached Slider TextBox

checking out AudioProcessorValueTreeState which is very nice. Wondering if there is a way to get the TextBox on the Slider to use the valueToTextFunction of the Parameter?

You could do that with a custom slider, I guess.

I’m finding myself in the same position – I love how everything ‘just works’ with the AudioProcessorValueTreeState workflow, but it would be really great to have direct access to the slider’s textbox and to provide it with the same text formatting functions. I also prefer being able to directly specify the locations of the two components, but maybe that’s just me. Here’s a solution I came up with which provides that functionality and plays nice with the existing workflow:

class CustomSlider : public Slider, public LabelListener
{
    class CustomSliderLabel : public Label, public SliderListener
    {
    public:
        CustomSliderLabel(CustomSlider* slider);
        ~CustomSliderLabel();
        
    private:
        void sliderValueChanged(Slider* slider) override;
    };
    
public:
    CustomSlider(Slider::SliderStyle sliderStyle,
                 std::function<String (float)> valueToText,
                 std::function<float (const String&)> textToValue);

    CustomSlider(Slider::SliderStyle sliderStyle,
                 const Rectangle<int>& sliderRegion, const Rectangle<int>& textBoxRegion,
                 std::function<String (float)> valueToText,
                 std::function<float (const String&)> textToValue,
                 const float fontSize = 9.0f);
    ~CustomSlider();
    
    Label* getTextBox();
    
    void setValueToTextFunction(std::function<String (float)> valueToText);
    void setTextToValueFunction(std::function<float (const String&)> textToValue);
    
    double getValueFromText(const String& text) override;
    String getTextFromValue(double value) override;
    
private:
    ScopedPointer<CustomSliderLabel> textBox;
    std::function<String (float)> valueToTextFunction;
    std::function<float (const String&)> textToValueFunction;
    
    void labelTextChanged(Label* textBox) override;
    void editorShown(Label* label, TextEditor& editor) override;
    void editorHidden(Label* label, TextEditor& editor) override;

    void visibilityChanged() override;
    
    std::atomic<bool> valueChangedViaTextbox;
};

CustomSlider::CustomSliderLabel::CustomSliderLabel(CustomSlider* slider) :
Label()
{
    slider->addListener(this);
    setJustificationType(juce::Justification::centred);
}
CustomSlider::CustomSliderLabel::~CustomSliderLabel()
{}

void CustomSlider::CustomSliderLabel::sliderValueChanged(Slider* slider)
{
    this->setText(slider->getTextFromValue(slider->getValue()), dontSendNotification);
}

CustomSlider::CustomSlider(Slider::SliderStyle sliderStyle,
                           std::function<String (float)> valueToText,
                           std::function<float (const String&)> textToValue) :
Slider(sliderStyle, Slider::TextEntryBoxPosition::NoTextBox),
textBox(new CustomSliderLabel(this)),
valueToTextFunction(valueToText),
textToValueFunction(textToValue),
valueChangedViaTextbox(false)
{
    textBox->addListener(this);
    textBox->setEditable(true);
}

CustomSlider::CustomSlider(Slider::SliderStyle sliderStyle,
                           const Rectangle<int>& sliderRegion, const Rectangle<int>& textBoxRegion,
                           std::function<String (float)> valueToText,
                           std::function<float (const String&)> textToValue,
                           const float fontSize) :
CustomSlider(sliderStyle, valueToText, textToValue)
{
    setBounds(sliderRegion.getX(), sliderRegion.getY(), sliderRegion.getWidth(), sliderRegion.getHeight());
    textBox->setBounds(textBoxRegion.getX(), textBoxRegion.getY(), textBoxRegion.getWidth(), textBoxRegion.getHeight());
    textBox->setFont(getLookAndFeel().getLabelFont(*textBox).withPointHeight(fontSize));
}

CustomSlider::~CustomSlider()
{}

Label* CustomSlider::getTextBox()
{
    return textBox;
}

void CustomSlider::setValueToTextFunction(std::function<String (float)> valueToText)
{
    valueToTextFunction = valueToText;
}
void CustomSlider::setTextToValueFunction(std::function<float (const String&)> textToValue)
{
    textToValueFunction = textToValue;
}

double CustomSlider::getValueFromText(const String& text)
{
    return textToValueFunction(text);
}

String CustomSlider::getTextFromValue(double value)
{
    return valueToTextFunction(value);
}

void CustomSlider::labelTextChanged(Label* textBox)
{
    if(valueChangedViaTextbox.load())
    {
        this->setValue(textToValueFunction(textBox->getText()));
    }
}

void CustomSlider::editorShown(Label* label, TextEditor& editor)
{
    valueChangedViaTextbox.store(false);
}

void CustomSlider::editorHidden(Label* label, TextEditor& editor)
{
    valueChangedViaTextbox.store(true);
}

void CustomSlider::visibilityChanged()
{
    textBox->setText(valueToTextFunction(this->getValue()), dontSendNotification);
}

*UPDATE: I removed the default, nullptr initialization for the textToValue and valueToText functions in the CustomSlider constructors upon realizing that I hadn’t done anything to deal with the case where the user doesn’t supply them. I figure it’s not bad to make them mandatory since the whole idea is to use the same ones that you supply to the AudioProcessorParameter object.

**Second update: Thanks to @lessp for identifying a bug where a value of 0 would result in an empty text box when opening the GUI window, I fixed it by overriding visibility changed and setting the textBox value there.

As a followup to help clarify the usage of this class within the overall AudioProcessorParameter workflow, here’s a recap of all the steps required to make sure this stuff works properly:

Remember that in your AudioProcessor (which must inherit ‘public AudioProcessorValueTreeState::Listener’), you first need calls like this to register your parameters in its constructor:

myAudioProcessorValueTreeState->createAndAddParameter(“myParamId”, “myParamName”, TRANS(“myParamName”),
myParamRange, myParamRange.snapToLegalValue(myParamDefaultValue),
myValueToTextFunction, myTextToValueFunction);
myAudioProcessorValueTreeState->addParameterListener(“myParamId”, this);

where myParamRange is a NormalisableRange object configured with the range of values you desire in your processor (that is, the values you want to make calculations on, not the range you might want to actually display in your GUI / in the DAW automation lane, that’s what the valueToText function is for).

Once you’ve added all your parameters, make sure you remember to initialize the AudioProcessorValueTreeState via:

myAudioProcessorValueTreeState->state = ValueTree(“myPluginName”);

Then, in your GUI component, you’ll need a ‘AudioProcessorValueTreeState::SliderAttachment’ object for each CustomSlider, which needs to be initialized something like this (in this example I’m using ScopedPointer to AudioProcessorValueTreeState::SliderAttachment and ScopedPointer to CustomSlider – make sure you’re passing the same instance of the AudioProcessorValueTreeState object that you’re using in your AudioProcessor):

mySliderAttachment = new AudioProcessorValueTreeState::SliderAttachment(myAudioProcessorValueTreeState,
“myParamId”, *myCustomSlider)

Make sure to addAndMakeVisible both the CustomSlider and its text box in your GUI constructor:

addAndMakeVisible(myCustomSlider);
addAndMakeVisible(myCustomSlider->getTextBox());

At this point, everything should be working properly and anything you type into the CustomSlider’s text box should make its way to the underlying Value object. Even if you don’t require any special formatting, make sure to remember to pass in textToValue and valueToText functions where needed! These are the generic functions I use:

std::function<String (float)> genericValueToTextFunction = [](const float v)
{
    return decimalToString(v, 2);
};

std::function<float (const String&)> genericTextToValueFunction = [](const String& s)
{
    return atof(s.toRawUTF8());
};

I hope that helps summarize the whole workflow for anyone trying this out!

Best,
Owen

1 Like

Hi Owen,
awesome recap: I am trying myself to come up with an optimal workflow that uses the AudioProcessorValueTreestate.

One thing I was struggling with (and by your recap I assume there is currently no solution) is avoiding the rewrite of these functions in both the processor and the editor.

In other words, the valuetotext/texttovalue lambdas set in AudioProcessorValueTreestate::createdandaddparameter are not accessible anymore via the ValueTreeState or the single AudioProcessorParameter.
So the editor instantiating the UI control either has them as global std::functions, or duplicates the lambdas code from the createandaddparameter.

Is that correct? [juce newbie and rusty C++ developer alert]

thanks,
Michelangelo

Hi Michelangelo,

I could be missing something myself but unfortunately I think you’re correct – currently there’s no way to retrieve those textToValue and valueToText functions from the AudioProcessValueTreeState after creating and adding the parameter, and yes if you’re passing them as lambdas then you would be repeating code. I’m keeping mine in a separate file & namespace which I include in both the AudioProcessor and AudioProcessorEditor, basically the global option.

If anyone is aware of a slicker way of doing this I’d be interested to hear about it, but unless your functions rely on capturing some variables from your processor in the lambdas, I don’t see it being a big deal to just define them once, someplace accessible to both the processor and the editor.

Best,
Owen

Try this: https://gist.github.com/yairchu/d1577c96f5b3c75e8831478a77d0a618
In addition to the attachment you need to call it’s setParameter method and then it uses the parameter’s string conversions. I’m using this extensively.

2 Likes

Yair,
Tested it right away - works like a charm! great approach too. I’ll use it extensively too :grin:
(just removed the SRsoftassert and the corresponding include - I’ll put a jassert instead to keep within the juce world)

Thanks guys - That was my first post in the Juce forum and I got a working answer in 8 hours…magic.

1 Like