Basic AudioProcessorValueTreeState plugin won't pass PluginVal

I have a basic AudioProcessorValueTreeState with just an AudioParameterBool and the PluginVal state restoration test fails with errors such as :

!!! Test 1 failed: Parameters not restored on setStateInformation -- Expected value within 0.1 of: 0.656967, Actual value: 0.963613

Below is a PIP to reproduce. Am I doing anything wrong?

PIP
    /*******************************************************************************
 The block below describes the properties of this PIP. A PIP is a short snippet
 of code that can be read by the Projucer and used to generate a JUCE project.

 BEGIN_JUCE_PIP_METADATA

 name:             GainPlugin
 version:          1.0.0
 vendor:           JUCE
 website:          http://juce.com
 description:      Gain audio plugin.

 dependencies:     juce_audio_basics, juce_audio_devices, juce_audio_formats,
                   juce_audio_plugin_client, juce_audio_processors,
                   juce_audio_utils, juce_core, juce_data_structures,
                   juce_events, juce_graphics, juce_gui_basics, juce_gui_extra
 exporters:        xcode_mac, vs2019

 moduleFlags:      JUCE_STRICT_REFCOUNTEDPOINTER=1

 type:             AudioProcessor
 mainClass:        GainProcessor

 useLocalCopy:     1

 END_JUCE_PIP_METADATA

*******************************************************************************/

#pragma once

//==============================================================================
class GainProcessor  : public AudioProcessor
{
public:
    //==============================================================================
    GainProcessor()
    : AudioProcessor (BusesProperties().withInput  ("Input",  AudioChannelSet::stereo())
                                       .withOutput ("Output", AudioChannelSet::stereo()))
    {
    }

    //==============================================================================
    void prepareToPlay (double, int) override {}
    void releaseResources() override {}
    void processBlock (AudioBuffer<float>&, MidiBuffer&) override {}

    //==============================================================================
    AudioProcessorEditor* createEditor() override          { return nullptr; }
    bool hasEditor() const override                        { return false;   }

    //==============================================================================
    const String getName() const override                  { return "Gain PlugIn"; }
    bool acceptsMidi() const override                      { return false; }
    bool producesMidi() const override                     { return false; }
    double getTailLengthSeconds() const override           { return 0; }

    //==============================================================================
    int getNumPrograms() override                          { return 1; }
    int getCurrentProgram() override                       { return 0; }
    void setCurrentProgram (int) override                  {}
    const String getProgramName (int) override             { return {}; }
    void changeProgramName (int, const String&) override   {}

    //==============================================================================
    bool isBusesLayoutSupported (const BusesLayout& layouts) const override
    {
        const auto& mainInLayout  = layouts.getChannelSet (true,  0);
        const auto& mainOutLayout = layouts.getChannelSet (false, 0);

        return (mainInLayout == mainOutLayout && (! mainInLayout.isDisabled()));
    }

    //==============================================================================
    AudioProcessorValueTreeState::ParameterLayout createParameterLayout()
    {
        std::vector<std::unique_ptr<RangedAudioParameter>> params;
        params.push_back (std::make_unique<AudioParameterBool> ("gain", "gain", false));
        return { params.begin(), params.end() };
    }

    void getStateInformation (MemoryBlock& destData) override
    {
        auto xml = std::make_unique<XmlElement> ("GainPlugin");
        xml->addChildElement (parameters.copyState().createXml().release());
        copyXmlToBinary (*xml, destData);
    }

    void setStateInformation (const void* data, int sizeInBytes) override
    {
        if (auto xml = getXmlFromBinary (data, sizeInBytes))
        {
            if (xml->hasTagName ("GainPlugin"))
            {
                if (auto stateXml = xml->getChildByName ("PARAMS"))
                    parameters.replaceState (ValueTree::fromXml (*stateXml));
            }
        }
    }

    AudioProcessorValueTreeState parameters { *this, nullptr, "PARAMS", createParameterLayout() };

    //==============================================================================
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (GainProcessor)
};

I have a code snippet online at juce-cookbook/working_with_juce/code_snippets.

It’s very similar, but my if condition is tree.isValid().

thanks!
But we have the same validation error with your code (PIP below).

PIP
/*******************************************************************************
 The block below describes the properties of this PIP. A PIP is a short snippet
 of code that can be read by the Projucer and used to generate a JUCE project.

 BEGIN_JUCE_PIP_METADATA

 name:             GainPlugin
 version:          1.0.0
 vendor:           JUCE
 website:          http://juce.com
 description:      Gain audio plugin.

 dependencies:     juce_audio_basics, juce_audio_devices, juce_audio_formats,
                   juce_audio_plugin_client, juce_audio_processors,
                   juce_audio_utils, juce_core, juce_data_structures,
                   juce_events, juce_graphics, juce_gui_basics, juce_gui_extra
 exporters:        xcode_mac, vs2019

 moduleFlags:      JUCE_STRICT_REFCOUNTEDPOINTER=1

 type:             AudioProcessor
 mainClass:        GainProcessor

 useLocalCopy:     1

 END_JUCE_PIP_METADATA

*******************************************************************************/

#pragma once

//==============================================================================
class GainProcessor  : public AudioProcessor
{
public:
    //==============================================================================
    GainProcessor()
    : AudioProcessor (BusesProperties().withInput  ("Input",  AudioChannelSet::stereo())
                                       .withOutput ("Output", AudioChannelSet::stereo()))
    {
    }

    //==============================================================================
    void prepareToPlay (double, int) override {}
    void releaseResources() override {}
    void processBlock (AudioBuffer<float>&, MidiBuffer&) override {}

    //==============================================================================
    AudioProcessorEditor* createEditor() override          { return nullptr; }
    bool hasEditor() const override                        { return false;   }

    //==============================================================================
    const String getName() const override                  { return "Gain PlugIn"; }
    bool acceptsMidi() const override                      { return false; }
    bool producesMidi() const override                     { return false; }
    double getTailLengthSeconds() const override           { return 0; }

    //==============================================================================
    int getNumPrograms() override                          { return 1; }
    int getCurrentProgram() override                       { return 0; }
    void setCurrentProgram (int) override                  {}
    const String getProgramName (int) override             { return {}; }
    void changeProgramName (int, const String&) override   {}

    //==============================================================================
    bool isBusesLayoutSupported (const BusesLayout& layouts) const override
    {
        const auto& mainInLayout  = layouts.getChannelSet (true,  0);
        const auto& mainOutLayout = layouts.getChannelSet (false, 0);

        return (mainInLayout == mainOutLayout && (! mainInLayout.isDisabled()));
    }

    //==============================================================================
    AudioProcessorValueTreeState::ParameterLayout createParameterLayout()
    {
        std::vector<std::unique_ptr<RangedAudioParameter>> params;
        params.push_back (std::make_unique<AudioParameterBool> ("gain", "gain", false));
        return { params.begin(), params.end() };
    }

    void getStateInformation (juce::MemoryBlock& destData) override
    {
        MemoryOutputStream stream (destData, false);
        parameters.state.writeToStream (stream);
    }

    void setStateInformation (const void* data, int sizeInBytes) override
    {
        ValueTree tree = ValueTree::readFromData (data, static_cast<size_t> (sizeInBytes));
        jassert (tree.isValid());

        if (tree.isValid())
            parameters.state = tree;
    }

    AudioProcessorValueTreeState parameters { *this, nullptr, "PARAMS", createParameterLayout() };

    //==============================================================================
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (GainProcessor)
};

Presumably the problem is that this line is not actually updating the gain value? At least maybe not synchronously?

yes, it’s a normalised/unnormalised issue :
you can call AudioParameterBool::setValue() with a normalised value (anything between 0 and 1), and the method AudioParameterBool::getValue() returns that normalised value.

But AudioProcessorValueTreeState set/recall the unnormalised value (0 OR 1), and doesn’t update the parameter value if the unnormalised value is the same.

So the issue is happening in such cases :

boolParam->setValue (0.6f); // value is at 0.6f
state = getValueTreeState();
boolParam->setValue (0.9f); // value is at 0.9f
setValueTreeState (state);  // the value isn't changed back to 0.6f because the unnormalised value is already 1

As the state restoration test of PluginVal is based on the sum of the normalised values it may fails if there is a ‘discrete’ parameter.
So either we should save/recall the normalised value and cannot use AudioProcessorValueTreeState::replaceState() as it is for now, either the PluginVal test should be relaxed a bit taking into account the discrete properties of the parameters.

Hmm, I’m not sure about this. If a param is set to a specific value, shouldn’t it return that value afterwards? I’m pretty sure auval has a check for this.

I could ignore discrete params from this test but that feels like the wrong approach…