AudioProcessorValueTreeState - Presets & undo/redo

Hi all

Relatively new to all of this so I fully expect, and will not be offended by, your derision and scorn.

I have developed an audio plugin with about 60 parameters, and I have used the same method as the Juce Demo plugin for adding parameters. I now want to implement a preset system, as well as undo and redo functionality. After browsing the forum for a while it appears that using an AudioProcessorValueTreeState would be the best way to accomplish this.

I experimented with this by taking one of my previous parameters and trying to reimplement it in the following way:

In PluginProcessor.h i declare the following (all public for the time being):

    AudioProcessorParameter* inputGain;

    UndoManager undoManager;
    AudioProcessorValueTreeState pluginState;

In PluginProcessor.cpp I initialise the AudioProcessorValueTreeState and attempt to create a parameter in the following way:

PluginProcessor::PluginProcessor()
    :undoManager(30000, 30),
     pluginState(*this, &undoManager)

{
    NormalisableRange<float> dbScale(-60.f, 6.f);
    inputGain = pluginState.createAndAddParameter ("inputGain", "InputGain", "Input Gain", 
                                                     dbScale, 0.f, nullptr, nullptr);

}

In my PluginEditor.h I declare a slider and slider attachment:

ScopedPointer<Slider> inputGainSlider;
ScopedPointer<AudioProcessorValueTreeState::SliderAttachment> inputGainSliderAttachment;

And in PluginEditor.cpp:

addAndMakeVisible(inputGainSlider = new Slider());
   
inputGainSliderAttachment = new AudioProcessorValueTreeState::SliderAttachment(owner.pluginState,
                                                                       "inputGain", *inputGainSlider);

After building this, it crashes my DAW (64-bit Ableton Live, Windows) as soon as I try and open the plugin. Upon further testing, even removing all the above code from my PluginEditor, removing the undoManager and simply creating and initialising my AudioProcessorValueTreeState:

PluginProcessor::PluginProcessor()
        : pluginState(*this, nullptr)
{

}

without creating and attaching any parameters results in my DAW crashing as soon as the plugin is loaded.

I reckon I am probably misunderstanding something basic and fundamental, so if anyone can name and shame what I have done please do so.

Further questions:

  1. Is an AudioProcessorValueTreeState the best way of implementing a preset and undo/redo system?
  2. Are there any preferable solutions, or could anyone point me in a different direction?
  3. (probably a separate issue…) I am using similar subclasses for sliders etc as is done in the JUCE Demo plugin, ie. ParameterSlider. My understanding is that the timerCallback() and updateSliderPos() functions are there to enable the GUI to respond to host automation, however when I test this using my plugin and the JUCE Demo plugin, neither of them can respond to automation while their GUI is open - instead they disable the host automation as soon as the first parameter change happens (again, this is using 64-bit Ableton Live in Windows - and again, it’s very possible I am the one misunderstanding or doing something incorrectly).

Any help or information would be greatly appreciated.

P.S I have booked my flight and ticket to the ADC in November, and am looking forward to meeting and learning from many of you there!

Hi gordcragg,

Could you please post the stack trace you get when your plug-in crashes? Can you get it to crash in JUCE’s Demo Host?

Some quick answers:

  1. Yes :slight_smile:

  2. This is probably due to an Ableton feature - “Back to Arrangement” https://www.ableton.com/answers/what-s-the-point-of-back-to-arrangement. We got around this for a few cases about a month ago (we saw the same behaviour in some of the the example plug-ins, perhaps you could copy what they do). Are you using the latest version of JUCE?

Hi Tom

Thank you very much for the response. I am struggling to figure out how to do a stack trace, but I think I uncovered the issue while debugging. A jassert is hit in the AudioProcessorValueTreeState class just as Ableton crashes:

static Parameter* getParameterForID (AudioProcessor& processor, StringRef paramID) noexcept
{
    const int numParams = processor.getParameters().size();

    for (int i = 0; i < numParams; ++i)
    {
        AudioProcessorParameter* const ap = processor.getParameters().getUnchecked(i);

        // When using this class, you must allow it to manage all the parameters in your AudioProcessor, 
        // and not add any parameter objects of other types!
 --->   jassert (dynamic_cast<Parameter*> (ap) != nullptr);

        Parameter* const p = static_cast<Parameter*> (ap);

        if (paramID == p->paramID)
            return p;
    }

    return nullptr;
}

The comment above the jassert seems to indicate what my issue is - I currently have a number of parameters that are not managed by my AudioProcessorValueTreeState (I use many instances of AudioParameterFloat, AudioParameterBool and AudioParameterChoice) and these are initialised using nullptrs. I assumeI will have to make all of my plugin parameters part of my AudioValueTreeState for it to work, is this correct? Will I be able to implement the AudioParametersBools (attached to buttons) and AudioParametersChoices (attached to combo boxes) using the AudioParameterValueTreeState??

Regarding automation recording, I am using the latest version of JUCE and had a look at how ParameterSlider is now implemented in the plugin demo. Automation for my sliders is now working in Ableton as expected, thank you! I am still having a few issues with buttons and combo boxes, but will work on fixing these when I get a chance.

Yes, you’re correct - you’ll need to add all of your parameters to your AudioValueTreeState. This will require a bit of work to migrate your existing AudioParameterXs to the new system, but attaching to buttons and combo boxes is easy. It’s worth doing! Serialisation and undoing events become trivial :slight_smile:

Definitely sounds worth it! Appreciate the assistance, I will begin the migration and post here again if I run into any complications (or if it all works perfectly!)

Hi all

I thought I would post this here since the title of the thread is still applicable to my new issue.

I have my plugin working perfectly using the AudioProcessorValueTreeState class, and am trying to implement undo/redo behaviour. I have come very close to getting it to work, but the bahaviour is very erratic and after days spent trying to debug, my last resort is to reach out for some assistance.

I am managing undo behaviour from my editor class. It seems that the UndoManager perform() method is called after (most) undo actions. To give a simplified example of a slider, this is basically what I am doing:

In order to separate transactions, when a slider drag ends I start a timer:

void sliderDragEnded (Slider* /*slider*/)
{
    startTimer(500);
}

When the timer callback is called, I call beginNewTransaction().

void timerCallback()
{	
    myProcessor->pluginState.undoManager->beginNewTransaction();
    stopTimer();
}

I have undo and redo buttons that act as follows:

void buttonClicked (Button* button)
{
    if (button == undoButton)
    {
        if (myProcessor->pluginState.undoManager->canUndo())
	    myProcessor->pluginState.undoManager->undo();
    }

    if (button == redoButton)
    {
        if (myProcessor->pluginState.undoManager->canRedo())
	    myProcessor->pluginState.undoManager->redo();
    }
}

In the event that I now call undo() from a button on the GUI, the AudioProcessorValueTreeState::setValue() method is called, which (as far as I can figure out) in turn calls the setProperty method, which then calls the perform() method.

void setProperty (const Identifier& name, const var& newValue, UndoManager* const undoManager)
{
    if (undoManager == nullptr)
    {
        if (properties.set (name, newValue))
            sendPropertyChangeMessage (name);
    }
    else
    {
        if (const var* const existingValue = properties.getVarPointer (name))
        {
            if (*existingValue != newValue)
                undoManager->perform (new SetPropertyAction (this, name, newValue, *existingValue, false, false));
        }
        else
        {
-->         undoManager->perform (new SetPropertyAction (this, name, newValue, var(), true, false));
        }
    }
}

So it seems that the undo() method recursively calls perform() which is specifically warned against in the UndoableAction documentation. This prevents the corresponding redo() action from being called and leads to a lot of strange behaviour.

I am not sure how to prevent this recursive call to perform() when an undo action is done. Am I using the UndoManager in the correct way with AudioProcessorValueTreeState, or am I doing something blatantly silly?

My guess is the contributors to this thread are encountering the same issue, see towards the end: AudioProcessorValueTreeState & UndoManager usage)

1 Like

A little more info… The difference between and undo action that works correctly and one that doesn’t seems to be what happens in the setProperty function. When an undo recursively calls perform(), it calls it from the setProperty function as I detailed in the post above.

When an undo action works as it should (I still can’t figure out when and why this happens some of the time but not all of the time), it reaches the if statement indicated below, and never calls perform.

void setProperty (const Identifier& name, const var& newValue, UndoManager* const undoManager)
{
    if (undoManager == nullptr)
    {
        if (properties.set (name, newValue))
            sendPropertyChangeMessage (name);
    }
    else
    {
        if (const var* const existingValue = properties.getVarPointer (name))
        {
-->         if (*existingValue != newValue)
                undoManager->perform (new SetPropertyAction (this, name, newValue, *existingValue, false, false));
        }
        else
        {
            undoManager->perform (new SetPropertyAction (this, name, newValue, var(), true, false));
        }
    }
}

Any help would be appreciated.

1 Like

Thank you Tom. I will test this when I have a chance.

Please put any feedback in the thread I linked; there are 4 or 5 different conversations about the same issue and it would be good to get everything in one place.