How to make A/B button in plugin without reseting undo history with APVTS?

Hello,
I am trying to make in my plugin A/B button to compare different parameters state.
But to do that I need to update all parameters in my AudioProcessorValueTreeState.

But it looks like the only method to that is to call AudioProcessorValueTreeState::replaceState().
But unfortunately it resets all undo history which I would like to avoid.

While I have access to AudioProcessorValueTreeState::state because it is public I tried to make my own method where I do the same as it is in AudioProcessorValueTreeState::replaceState() but without reseting undo history but of course it doesn’t work because when I assign new ValueTree to the AudioProcessorValueTreeState::state it probably loose all listeners or something like that. So I am not sure how to do that.

Has anybody any solution to use when we want to exchange all parameters values without losing undo history?

For any help great thanks in advance.

Best Regards.

Think that come to my mind is to go through each parameter in APVTS and setProperties by myself.
But I am not sure if it’s proper solution because I’ve seen in APVTS documentation there is advice to use replaceState() if we want safely call setStateInformation.
I am not sure what is setStateInformation, but I suppose they mean ValueTree::setPropertie. So I end up on the begining of my issue.

The last think that come to my mind is go through each AudioProcessorParameter and call setValueNotifyingHost().

But I still wonder if there isn’t any simpler solution.

copyPropertiesAndChildrenFrom() works and doesn’t clear the history. Just don’t do the a/b swap from anywhere that isn’t the message thread, as you lose the nice thread safety stuff going on in replaceState().

Juce devs: how trivial would it be to get a flag on the replace state call to avoid clearing the undo queue?

Hmm… Fandusss, great thanks for your reply. But actually it doesn’t work for me. I tried that earlier.
The problem is that inside:
ValueTree::SharedObject::callListeners()
there is called:
auto numListeners = valueTreesWithListeners.size();

And numListeners are ok until I call
APVTS.state.copyPropertiesAndChildrenFrom(ab_StorageValueTree, nullptr)

After that the numListeners are equal to zero. So I suppose that while copyPropertiesAndChildrenFrom() removes all current children (by calling removeAllChildren (undoManager);) it also removes theirs listeners or something like that.

Or maybe I do something wrong.

My code is like that. I have objects:

UndoManager myUndoManager;
AudioProcessorValueTreeState myAPVTS;
ValueTree ab_Storage;

In my AudioProcessor constructor I define myAPVTS, and later also in constructor I call:

// To avoid jassert (object != nullptr || source.object == nullptr); in copyPropertiesAndChildrenFrom()
ab_Storage.ValueTree("TMP");

// To keep the same A and B variants on plugin instatiation
ab_Storage.ValueTree.copyPropertiesAndChildrenFrom(myAPVTS.state, nullptr);

In case there is saved state I also call on the end of setStateInformation():

ab_Storage.ValueTree.copyPropertiesAndChildrenFrom(myAPVTS.state, nullptr);

Then in perform() of AB UndoableAction I call:

    ValueTree _tmp("TMP");
    _tmp.copyPropertiesAndChildrenFrom(myAPVTS.state, nullptr);

    ScopedLock lock (valueTreeChanging);
    myAPVTS.state.copyPropertiesAndChildrenFrom(ab_Storage, nullptr);

    ab_Storage.copyPropertiesAndChildrenFrom(_tmp, nullptr);

And after that, when I call myUndoManager.undo(); everything is fine (It perform again the action described above). But when I call again myUndoManager.undo(); then it doesn’t call listeners. So actually there is undo history, but it’s useless if it doesn’t call any of AudioProcessorParameter::Listener.

And of course I also checked what happen if in AB action I call for each parameter setValueNotifyingHost().
And it also doesn’t work for me because - I am not sure but it looks like - setValueNotifyingHost() also call some UndoManager actions (which call perform()) so when I undo AB I get jassert isPerformingUndoRedo() inside perform().

I’ve spent a lot of time on undo/redo (with the apvts undomanager). This stuff can go very deep. My recommendation is to first be completely clear on the high level expectation of the behavior you want.

When I click the A/B button, what is the expectation?

  1. Does it add an UndoableAction? Can I “undo” the compare? If so, then you’ll want to register a new transaction with the UndoManager and then use copyPropertiesFrom. For example, loading presets could work this way, treating the preset load as something that you can undo:
apvts.undoManager->beginNewTransaction ("Change Preset");
apvts.state.copyPropertiesFrom (otherState, undoable ? apvts.undoManager : nullptr);

I tend to call a function to do this with an undoable boolean as there are times I want to register an UndoableAction (preset load) and other times where I do not (setStateInformation).

I also had issues with the values not being set, so I actually then iterate over the tree and manually populate the values… unideal but it gets the job done.

  1. Does it rollback to compare with a prior state that’s in the undo history? In that case, could you just call undo X times?

  2. Does a “compare” completely ignore the UndoManager? In that case, what’s the expected behavior if I hit compare and then undo? This seems like it could have some edge case behavior that would be annoying to solve, but in theory can be solved the same way as #1, with undoable set to false.

Hello sudara.
Of course there are various solutions and expectations on A/B comparison. I try to explain my case and my expectation of A/B behaviour. So it looks like that:

Let’s say my plugin has only two parameters, let’s say input and output volume, both with default value 0dB. In addition plugin have A/B button (toggleable button, with OFF state corresponds to A state, and ON state to B state). There are also undo button and redo button which of course are not toggleable. And of course all three buttons are not attached to any APVTS params because the only APVTS params are input volume and output volume.
User start totaly new instantion of my plugin, which on start should have both A and B states with the same params states set to 0dB. And buttons UNDO and REDO are inactive while there are no undoable steps. And now steps and expectations are:

  1. first step: being on A state, user change INPUT volume to -6dB.
    expectations: UNDO button now is active with only one possible action to undo. But REDO button is still inactive

  2. second step: still on A state user change OUTPUT volume to -12dB. So user made two steps and now on A state both params are different than in B state where both params are still set to 0dB.
    expectations: UNDO button is still active but with two possible actions to undo. REDO button is still inactive.

  3. third step: user click A/B button
    expectations: A/B button is changed to ON state (which correspond to params B state), and now user should have again both parameters set to 0dB. But it happened only in one step. UNDO button is still active but possible actions to undo raised only ones, so there are three possible actions to undo (even though there are two params changed in one click). REDO button is still inactive.

  4. fourth step: user click UNDO.
    expectations: A/B button should become again set to OFF state (which corresponds to params A state). So INPUT volume should be set to -6dB and OUTPUT volume set to -12dB. All changes in one step. UNDO button num of possible actions decrease by one. So there are now two possible actions to undo. REDO button became active with one possible action to redo.

  5. fifth step: user click UNDO again and now OUTPUT volume should change to 0dB, but INPUT is still set to -6dB.
    expectations: UNDO button num of possible undu actions decrease by one, and now there are only one possible action to undo. REDO button num of possible actions raised by one, and now there are two actions to redo.

  6. sixth step: user click UNDO again, and now both params should be set to 0dB. as it was on the beginning.
    expectations: now UNDO button should become inactive because there is no more actions to undo. REDO button num of possible actions raised by one, and now there are 3 actions to redo. Clicking rapidly on REDO button should repeat all steps in reverse direction, I mean from 6 to 3.

These are my expextations which seems to me quite obvious, but I can’t achieve such behaviour from about 3 days, and it is really frustrating.

And to be clear apvts.state.copyPropertiesFrom() I suppose should also not work because it copies only properties, but params are actually children of apvts.state, not properties.
So I expect I should iterate through all children of apvts.state and for each one call childOfApvtsState.copyPropertiesFrom(). Am I right or not?

In the other hand calling copyPropertiesAndChildrenFrom in first step (not first but one before copying children) remove all children from current value tree, and I am not sure what happens with the listeners of those removed childern

I understand this and relate! I spent several weeks solid on a big handful of undo/redo complexities in my app (I track UI state as well, and had to figure out a lot of edge cases).

Nice description of your needs — very clear!

It sounds like switching between states is something you are treating as “undoable” so you’d manually begin a UndoManager transaction before you switch states.

Sorry I was unclear.

I had to use copyPropertiesFrom and then manually iterate through the values, because of that issue I had with copyPropertiesAndChildrenFrom not copying the values over. To be honest, I don’t 100% remember the details anymore, but my working implementation looks like this:

apvts.state.copyPropertiesFrom (otherState, apvts.undoManager);

// Walk through our parameters and update with any new values
// We'll treat our apvts as being a "whitelist" of parameters
// We have to do this because the apvts doesn't reconstruct params after assignment
// https://forum.juce.com/t/bug-apvts-doesnt-fully-sync-missing-parameters-on-assignment/51509
for (auto i = 0; i < apvts.state.getNumChildren(); ++i)
{
    auto id = apvts.state.getChild (i).getProperty ("id");
    auto incomingChild = otherState.getChildWithProperty ("id", id);
    auto incomingValue = incomingChild.getProperty ("value");
    
    // Only replace the apvts child if it exists and has a valid value
    if (incomingChild.isValid() && !incomingValue.isVoid())
    {
        apvts.state.getChild (i).setProperty ("value", incomingValue, apvts.undoManager);
    }
    else
    {
        DBG ("incoming state was invalid, id: " << id.toString() << " value: " << incomingValue.toString());
    }
}

Sudara, great thanks for your in deepth explanation. Really appretiate for that. Now I need to analyse it and test. I will inform about results. At first glance it looks for me like something I’ve already tried without success. But maybe I missed something. Now I have much cleaner mind and I need to try it again.

1 Like

Hello again sudara.
Now I see where the problem is (I am not sure if it’s only onle problem but at least one).

I found out that ValueTree::fromXml() dismiss all properties types. I made test as follows:

myValueTree.setProperty(propID, 10.0f, nullptr);
var propertyValueTest1 = myValueTree.getProperty(propID);

std::unique_ptr<XmlElement> testXml = myValueTree.createXml();

myValueTree = ValueTree::fromXml(*testXml.get());
var propertyValueTest2 = myValueTree.getProperty(propID);

bool isValue1Double = propertyValueTest1.isDouble(); // This is TRUE
bool isValue2Double = propertyValueTest2.isDouble(); // This is FALSE

So it looks like I need to remember types by myself.

But what is also interesting I made one more test:

const float floatVal1 = propertyValueTest1;
const float floatVal2 = propertyValueTest2;

bool areEquals = floatVal1 == floatVal2; // That is TRUE, and both values are equal 11.0

So in some way ValueTree::fromXml remembers the types but they are “encrypted” in some way as a string value. I mean encrypted because when I debug var propertyValueTest2 value under variable value there is not 11 (as it should) but it is stored in stringValue as “0x70 0x04 0x24 0x00 0x00 0x60 0x00 0x00”.

But actually it gives me nothing because when I call:

myValueTree.setProperty(propID, propertyValueTest2, nullptr);

It doesn’t send any change messages while inside of setProperty there is checking if original type is the same as new one. And it is not, that’s why I still need to remeber types when recalling states.

Or maybe I do something wrong.

For any help great thanks in advance.

Best Regards