Undo broken after calling APVTS replaceState with an empty state

Hi,

Calling AudioProcessorValueTreeState::replaceState with a valid but empty ValueTree causes the internal state of the APVTS to contain PARAM children with only id properties—no value fields for params that were previously set to their default values. This can be easily reproduced by creating an APVTS instance and immediately calling replaceState with an empty ValueTree.

Why is this a problem?

If a PARAM has no value property, then the first time it changes, the new value is written to the tree via tree.setProperty (key, unnormalisedValue.load(), nullptr); inside AudioProcessorValueTreeState::ParameterAdapter::flushToTree. Since no UndoManager is passed to setProperty, this write isn’t tracked—breaking the first undo operation.

Here’s how this manifests in our app:

  1. App starts.
  2. User clicks “Reset to Default”.
  3. Our implementation of “reset to default” calls replaceState with an empty ValueTree.
  4. User moves a slider.
  5. User hits Undo.
  6. Nothing happens—Undo fails.

Please can this be fixed?

Best,

Jelle

1 Like

I believe this is the root of your issue.

In all of our plugins (and in our plugin hosts) we just store the default preset immediately after construction.

This is important because you want to use logical parameter values that the plugin has set in it’s constructor and not just ‘0’ on all parameters which in many cases isn’t the proper init - some parameters might be init in the middle, etc.

The AVPTS knows of all the default values for all parameters. When replaceState is called with an empty state, the APVTS uses the default values of all parameters and stores those. This works, except when, as explained, the parameters were previously set to their default values.

See examples below. Notice that for case 2, the value is missing in the xml. The reason is that the new value for the state is still the same as the previous default value, preventing a parameterValueChanged callback inside AudioProcessorValueTreeState::ParameterAdapter.

// case 1
juce::AudioProcessorValueTreeState state; // constructing state with one param with default value of 0.5
state.state.toXmlString(); // <State> <PARAM id="param" value="0.5"/> </State>

// case 2
juce::AudioProcessorValueTreeState state;
state.replaceState(juce::ValueTree("State"));
state.state.toXmlString(); // <State> <PARAM id="param"/> </State>

// case 3
juce::AudioProcessorValueTreeState state;
state.getParameter("param")->setValueNotifyingHost(0.1f);
state.replaceState(juce::ValueTree("State"));
state.state.toXmlString(); // <State> <PARAM id="param" value="0.5"/> </State>
1 Like

The default values for the parameters aren’t the real default init preset of a plugin.
The default value for a parameter is what’s used usually when the user alt+clicks a parameter in a generic editor.

However - the init preset of the plugin might (and many times will) change this to something more contextual - for example a distortion plugin might have an init with some amount of drive, while the ‘default’ of the ‘gain’ parameter would be 0.

I think the correct way for a plugin to handle this is to store the contextual init preset - you also have to remember non-parameter values also exist and APVTS knows nothing about the defaults for those.

I’m not concerned with what an init preset is or how to implement default or init presets.

What I’m describing is a bug in JUCE: the internal state structure of the AudioProcessorValueTreeState (APVTS) after calling replaceState depends on the previous state. As shown in my previous example (case 2 and case 3), calling replaceState twice with the exact same juce::ValueTree("State") results in different internal ValueTrees within the APVTS. This inconsistency causes issues in our application.

While we’ve implemented a workaround, we believe this is something JUCE should address to ensure consistent behavior.

I think the only bug in JUCE here that it (IMO) should just totally ignore (and maybe assert?) for an empty ValueTree and not load a default preset in that situation.

So far, we’ve only talked about empty ValueTrees, but there are situations where a previously saved plugin state might be missing some parameter values, due to a plugin update. The same issue occurs there, and APVTS does a good job of resetting parameters to their defaults when it detects missing parameters.
That said, I really appreciate being able to pass an empty ValueTree to replaceState and have everything reset to default values. It’s a great feature overall and works fine, aside from the minor (but problematic) issue described in this post.

1 Like

I think it is also fair to say that not all plugins have an init preset that differs from the default state. So in that case, simply resetting everything to default will be the same as loading the init state. For those plugins, having to store and load a seperate init preset would add unnecessary code. This approach of loading and empty ValueTree (or a seperate resetToDefault function) would be a lot more usefull

JUCE team, is this post seen and noted as a bug? Or should we accept the current behaviour and stick with our own workaround?