Custom Attachment Class Resetting Values When GUI is Reopened

Hey all,

I’m relatively new to JUCE (<1 year) but I’ve been building a plugin and I think it’s coming along really nicely. I tried building a custom slider, and it works in every aspect except for when the GUI is closed and reopened again. All the values on the slider reset to their default values, which is then sent onto the processor. This doesn’t happen for any of my other sliders, as I’m using the APVTS to maintain consistency across their values.
I’ve spent hours debugging, trying to find where the switch occurs, and I just can’t figure it out for the life of me. I’m using REAPER as my debug environment. Here’s what happens:

  1. I open up the plugin and change the value on the slider.
  2. I close the GUI
  3. I open up the GUI again, to which Visual Studio executes a breakpoint and all processing stops.
  4. I check all the values for the slider, which remain correct even when the editor is reinstantiated in juce_audio_client_vst3.cpp.
  5. I step ahead until I reach a screen telling me that the debugger can’t open reaper.pdb. The values are still consistent but I can’t use the debugger to go forward anymore
  6. I go back and add a conditional breakpoint to the slider’s value and hit continue to find where it changes
  7. The slider’s value changes back to it’s default, and the call stack just consists of internal message broadcasting stemming from reaper.pdb except for when it hits .onValueChange() attachment class.

I have no idea why it’s switching back to its default here. The slider is part of a separate component, but I’m instantiating it the same way I do all my other sliders, whose values remain consistent. The slider appropriately changes the values I want it to change using the APVTS. Here’s the code for the attachment class I made:

#include <JuceHeader.h>

class TwoValueSliderAttachment : AudioProcessorValueTreeState::Listener
{
public:
    TwoValueSliderAttachment(AudioProcessorValueTreeState& apvts,
        const String& minParamID,
        const String& maxParamID,
        Slider& slider)
        : apvtsRef(apvts), minParamID(minParamID), maxParamID(maxParamID), sliderRef(slider)
    {

        jassert(slider.getSliderStyle() == Slider::SliderStyle::TwoValueHorizontal);

        minParam = apvtsRef.getParameter(minParamID);
        maxParam = apvtsRef.getParameter(maxParamID);

        jassert(minParam && maxParam);
        
        updating = true;

        //Set Initial Values
        sliderRef.setMinAndMaxValues(
            minParam->convertFrom0to1(minParam->getValue()),
            maxParam->convertFrom0to1(maxParam->getValue()),
            dontSendNotification);

        updating = false;

       /* Sync Slider to parameters*/
        this->sliderRef.onValueChange = [this]() {
            if (this->updating) return;

            this->updating = true;

            if (this->minParam != nullptr)
                this->minParam->setValueNotifyingHost(this->minParam->convertTo0to1((float)this->sliderRef.getMinValue()));

            if (this->maxParam != nullptr)
                this->maxParam->setValueNotifyingHost(this->maxParam->convertTo0to1((float)this->sliderRef.getMaxValue()));

            this->updating = false;
            };

        DBG("Slider triggered: " << slider.getMinValue() << " " << slider.getMaxValue());



        apvtsRef.addParameterListener(minParamID, this);
        apvtsRef.addParameterListener(maxParamID, this);

    }

    ~TwoValueSliderAttachment() {

        apvtsRef.removeParameterListener(minParamID, this);
        apvtsRef.removeParameterListener(maxParamID, this);

    }

    void parameterChanged(const juce::String& parameterID, float newValue) override
    {
        if (updating) return;
        updating = true;

        if (parameterID == minParamID)
            sliderRef.setMinValue(newValue, juce::dontSendNotification);
        else if (parameterID == maxParamID)
            sliderRef.setMaxValue(newValue, juce::dontSendNotification);

        updating = false;
    }

private:

    AudioProcessorValueTreeState& apvtsRef;
    String minParamID;
    String maxParamID;
    Slider& sliderRef;

    RangedAudioParameter* minParam = nullptr;
    RangedAudioParameter* maxParam = nullptr;

    bool updating = false;

};

I am not sure about the issue. But you should NOT call sliderRef.setMinValue and sliderRef.setMaxValue inside the function parameterChanged(). parameterChanged() can get called from the audio thread.

Besides, you should call beginChangeGesture() and endChangeGesture() before/after the setValueNotifyingHost(). In fact, for a slider, it should be something like this:

void sliderValueChanged(juce::Slider *) override {
    const auto normalized_value = parameter_ref_.convertTo0to1(static_cast<float>(slider_.getValue()));
    parameter_ref_.setValueNotifyingHost(normalized_value);
}

void sliderDragStarted(juce::Slider *) override {
    parameter_ref_.beginChangeGesture();
}

void sliderDragEnded(juce::Slider *) override {
    parameter_ref_.endChangeGesture();
}

I took a week off but I’m back; I changed the code in the attachment class to more closely match what you recommended, but it didn’t fix the issue. I’ve spent some more time debugging and attaching breakpoints to the values of the slider (I should have mentioned I’m using a slider with two heads), and they lead to .dll files I can’t access.

I tried testing it in MyPluginVal and it says all tests are passing on strictness level 10, but I can see at the end when the plugin closes and opens several times, the values reset again.

Is there any other environment where I can try to see what’s happening during these inaccessible files? It seems like that’s where the values are changing, and it’s so confusing that it’s only happening to this attachment and none of the other default ones.

Okay, so you are trying to debug a plugin. There are several ways:

  1. Use `juce::FileLogger` to print some values to a file
  2. Use DBG to print values and run the plugin (in debug mode) in the DAW (which IDE attaches to), see The big list of JUCE tips and tricks (from n00b to pro) · Melatonin
  3. Use breakpoint, build the plugin (in debug mode) and run plugin in pluginval (in debug mode), see pluginval/docs/Debugging a failed validation.md at develop · Tracktion/pluginval · GitHub