AudioProcessorValueTreeState & UndoManager usage


#1

Hi,

As one who's still a newbie I find it hard sometimes without concreate demo like the ones included within JUCE.

So I've made this demo plug-in for my understanding and also in-order to get some feedback from all the great JUCErs outhere who might be able to chime in.

https://github.com/talaviram/juce-audio-sample-plugins/tree/master/AudioProcessorValueTreeStateDemo

What I've yet to understand is how to use to UndoManager and the ValueTree for saving/loading presets.

Also, when using the SliderListener wouldn't it be wiser to call the text function instead of the rawValue (look at the sample&hold <> sample switch in my example or my samplerate utilization/workaround for keeping it updated value).

one note, I've yet to test the project on Windows.

 

any help on how to get the UndoManager going would be helpful.

Thanks!


AudioProcessorValueTreeState - Presets & undo/redo
#2

bump :)

anyone got a clue why the UndoManager not working as I'd expect it to?


#3

Expecting people to spend time looking through your project to debug it is a big ask - maybe explain in more detail what's going wrong?


#4

have a look at the UndoManager and ValueTree class documentation.  

if you can't figure it out, there's example code to follow in the "Getting Started With Juce" book.  

 

https://www.packtpub.com/application-development/getting-started-juce


#5

not a general tutorial, but here's a little but nasty gotcha with AudioProcessorValueTree and UndoManager that could have saved me hours of debugging if I'd had known it in advance, and that you might run into as well: 

when you call 

AudioProcessorValueTreeState::createAndAddParameter (..., NormalisableRange<float> range, float defaultVal, ...)


you should make absolutely sure that the defaultVal is created by range.snapToLegalValue(yourDefaultNormalisedValue) !

if you don't do this, then the following scenario might happen during undo/redo:

0. start with the given default value after parameter creation 

1. change the parameter value N (with N >= 2) times to create an undo chain. (no problem here)

2. undo N-1 times (no problem either, Redo will work fine after each of these)

3. undo N times (so back up to your initial default val): BOOM!, the redo history will be destroyed and no redo will be possible now

 

Why is this happening?

if the defaultVal for the parameter is not produced via snapToLegalValue() then at the moment you reach that defaultVal in the undo chain the UndoManager will set the valuetree back to your original (nonsnapped) defaultVal, but the corresponding AudioProcessorParameter will at that point have a value that IS a snapped version of this.  So the AudioProcessorValueTree timer will  be notified that the valuetree param value is out of sync (though very slightly) with the audioparameter, and tries to update the valuetree.  So this last undo causes a NEW value in the valuetree and hence a new undo entry, destroying any future redo transactions

subtle perhaps, but certainly hair loss inducing

the remedy is to religously check that you always snap your default value for the parameter to a legal value

suggestion to Jules & colleagues:  since the AudioProcessorValueTree::Parameter also gets the range object handed as a parameter, the parameter ctor could theoretically snap the defaultvalue itself as a precaution. 

 

 


#6

Thanks mucoder!

 

@Jules, ofcourse I don't expect anyone to debug my code. the only reason I've published this project is to help newbies like me to see the new AudioParametersValueTreeState usage with hope someone from JUCE/ROLI could chime-in if this is the proper way to initialize it. since the behavior looks a little odd.

The state itself and listeners works really well but I thought that it would "auto-magically" support undo/redo without much code.

Since I guess most people won't have energy to look at the demo project I've made here are the main parts:

PluginProcessor.h has the following objects:

UndoManager undoManager;

AudioProcessorValueTreeState parameters;

Default constructor in PluginProcessor.cpp:

undoManager(30000,30),parameters(*this,&undoManager)

Init within the PluginProcessor.cpp constructor:

auto textValueForDecibel = [](float val) -> String { String rawString = String::formatted("%2.1f",val); rawString.append("dB", 2); return rawString; };

NormalisableRange<float> clipRange = NormalisableRange<float>(-30.0f,0.0f); parameters.createAndAddParameter(PARAM_ID_CLIP, PARAM_NAME_CLIP, PARAM_NAME_CLIP, clipRange, clipRange.snapToLegalValue(0.0f), textValueForDecibel, nullptr);

// set-up ValueTree for saving/loading/undo

parameters.state = ValueTree(String("ValueTreeStateDemo"));

parameters.undoManager->clearUndoHistory();

parameters.undoManager->redo();

(the reason I'm clearing undo history is the it starts dirty I don't undetstand why..)

 

getStateInformation:

DBG("Store XML");

ScopedPointer<XmlElement> xml(parameters.state.createXml());

copyXmlToBinary (*xml, destData);

setStateInformation:

ScopedPointer<XmlElement> xmlState (getXmlFromBinary (data, sizeInBytes));

parameters.state = ValueTree::fromXml(*xmlState);

parameters.undoManager->clearUndoHistory();

The save/load ofcourse works like a charm and seems very neat as I can also add non-parameters to the valuetree by myself ;)

 

PluginEditor.h has:

ScopedPointer<Slider> clipSlider;

ScopedPointer<AudioProcessorValueTreeState::SliderAttachment> clipParamAttach

in PluginEditor.cpp constructor:

clipParamAttach = new juce::AudioProcessorValueTreeState::SliderAttachment (processor.parameters, PARAM_ID_CLIP, *clipSlider);

It implements button listeners for TextButtons - Undo/Redo so my listener basically does that:

else if (buttonThatWasClicked == undoBtn)

{

    if(processor.parameters.undoManager->canUndo())

   {

        processor.parameters.undoManager->undo();

    }

}

Same applies for redo.

I don't understand the behavior as for example: (the values are examples since my range is different ofcourse...)

1. I click the slider set it from 1.0 to 0.7 then to 0.5 then to 0.3

2. click undo it'll jump to 1.0

---- or

1. I click the slider it set it from 1.0 to 0.5...

2. click undo, works as expected.

3. click redo, does nothing (I'd expect the value to be set to 0.5 again...)

 


Any examples of using AudioProcessorValueTreeState for handling parameter changes?
#7

Did you figure out why redo() didn’t work? I also just got undo() to work, but it’s also my first time to use the undoManager(), so I’m not sure about the usage. It would be great, if someone could offer a short basic example.


#8

@marvu I have yet to get back to it.
But I’ll do it soon as we have it in an upcoming product and even without AudioProcessorValueTreeState I see some behavior that requires more investigation…


#9

OK thanks. Please let me know when you figured something out.


#10

Almost 2 years and it seems like Undo/Redo with AudioProcessorValueTree is still isn’t ready for “prime-time” or am I missing something?

AudioProcessorValueTree looked very promising but still I see that those threads are still left open and all gets to pretty much same recursive calls (getting asserts), broken redo functionality, etc…



(here @fabian explicitly mentioned there’s something funky due to multiple value changes).

So should I simply create my own UndoManager and avoid using the AudioProcessorValueTree.
That way I can get only relevant calls. since undo/redo is related only to UI elements rather than actual parameters.
(since if you listen to parameter changes also automation might register…)


#11

Not really sure where you’re getting that idea from… There are lots of solid commercial field-tested plugins out there using it, including our own Equator, which I know has been heavily used by many thousands of people for a couple of years, and which has undo/redo.


#12

UndoManager is very mature. I’m explicitly mentioning AudioProcessorValueTreeState usage of it.

I’ve made now another test on my personal machine with clean JUCE 5 (master 7e959).

Plain “plug-in” with only gain slider and undo redo buttons.
For the sake of keeping it short I’m only showing the important editor parts.

That’s my constructor:

addAndMakeVisible(undoBtn);
addAndMakeVisible(redoBtn);
addAndMakeVisible(gainSlider);

undoBtn.addListener(this);
redoBtn.addListener(this);

gainAttach = new AudioProcessorValueTreeState::SliderAttachment(p.params,"gain",gainSlider);

setSize (400, 200);

That’s my a simple listener for the buttons:

void AudioProcessorValueTreeUndoAudioProcessorEditor::buttonClicked (Button* btn)
{
    UndoManager* undoMgt = processor.params.undoManager;

    if (btn == &undoBtn)
        if (undoMgt->canUndo()) undoMgt->undo();

    if (btn == &redoBtn)
        if (undoMgt->canRedo()) undoMgt->redo();
}

On the processor side,
Constructor inits:

,params(*this, &undoManager)
{
    params.createAndAddParameter("gain", "Gain", "gain", NormalisableRange<float>(0.0,1.0), 1.0, nullptr, nullptr);
    params.state = ValueTree( Identifier("undoTest"));
} 

And you get those behaviors:

  1. Click Undo just when running it first time will assert (UndoManager::perform:126) as:

             jassertfalse;  // don't call perform() recursively from the UndoableAction::perform()
                        // or undo() methods, or else these actions will be discarded!
    

I can “overcome” those assertions by adding on our constructor:

undoManager.clearUndoHistory();
  1. Now it pretty much “works”. but there are more undoable/redoable actions than what I’d expect from the plug-in…
  • start the plugin
  • move slider
  • undo
  • Try to redo, it’ll fail.
  • start the plugin
  • move slider
  • move slider
  • Try undo, it’ll consider both moves as a single transaction…

I didn’t try equator but if you compare this to other plug-ins undo/redo. this isn’t a common workflow. or am I’m implementing it wrong?


(Possible bug) Clearing AudioProcessorValueTreeState undo history after setting state
#13

note that “Redo” does not currently work correctly with AudioProcessorValueTreeState :


#14

That’s why I’ve asked about the status of UndoManager within AudioProcessorValueTree context…
Because from my experiments it seems safest undo/redo with current state of it would be for me to implement UndoManager separately from AudioProcessorValueTree.

So unless I’m not implementing it wrong I’m confused with @jules saying it is being used with Equator/field-tested plugins.

Again,

  • we’ve used UndoManager with our current parameter engine and it’s working well.
  • we’re now using UndoManager with a ValueTree and it’s also seems to be doing well.

I’m only asking about AudioProcessorValueTree and UndoManager…


#15

Hi,

in my AudioProcessor I have a AudioProcessorValueTreeState and and UndoManager. I pass the undoManager into the AudioProcessorValueTreeStates constructor and create all my parameters with state.createAndAddParameter. In my plugin editor, I use AudioProcessorValueTreeState::SliderAttachment. I want a new undo transaction to start, each time a new slider-drag begins, so I do

slider.onDragStart = [&](){
    std::clog << "new transaction:" << slider.getName() << std::endl;
    filter->undoManager.beginNewTransaction(slider.getName());
};

I also have two buttons labeled ‘undo’ and ‘redo’. that trigger undoManager.undo() and undoManager.redo() respectively.

All this works quite nicely, there are however some oddities:

  1. canUndo() returns true, even at the very beginning. This is because flushParameterValuesToValueTree() is called by some timer and perform()s SetPropertyActions on the undo manager. because this is done via a timer, I can’t even do undoManager.clearUndoHistory(); in the constructor of my AudioProcessor
  2. while undo works as expected, redo sometimes is messed up by flushParameterValuesToValueTree as well. If the timing is ‘just right’ (i.e. just wrong), flushParameterValuesToValueTree() is called right after I press my undo button and this pushes new actions into the undo manager and this trashes the redoable actions.

@jules: are the AudioProcessorValueTreeState & UndoManager supposed to work together? Is this a tested/supported scenario? Is there an example somewhere?


#16

This is a known issue, and @t0m has said it will be fixed in a future overhaul to AudioProcessorValueTreeState:

I (and others) have worked around it by adding a small delay to clear the undo history shortly after object creation, which while a total hack seems to clear it up. See my workaround at line 54:


#17

You can give AudioProcessorUndoAttachment a try instead, it works better for AudioProcessorValueTreeState than AudioProcessorValueTreeState's built-in UndoManager support.


#18

great, thanks!


#19

Thanks a ton, I will! How is your gist licensed?


#20

Public domain :slight_smile: Or if that definition doesn’t work for you then BSD license.