How to safely attach UI components to non-automatable plugin state with undo history and persistence?

I have been sharing state between my plugin processor and plugin editor two ways:

  1. APVTS, Parameters, and ParameterAttachments (using patterns found in the official JUCE tutorials and examples)
  2. Atomic variables, and a timer in the editor that periodically updates the UI based on the state of the atomic vars

This works fine, but I have need for something not covered by these approaches. My requirements are:

  • Manage internal state that is not exposed to the host and not automatable
  • Internal state changes need to be persisted with parameters in the DAW project
  • Internal state changes need undo history that works with parameter changes

This should be possible, right?

DAW automation and Parameters seem tightly coupled (there’s a thread here about how DAWs ignore attempts to hide parameters: Using a ParameterAttachment for non-automatable parameters? - #5 by HowardAntares),
so it seems I need to avoid Parameters for this use case.

I’m thinking I should be using a ValueTree that I append to the apvts.state so it can share an UndoManager with the Parameters. I guess I want something similar to ParameterAttachment that works with arbitrary ValueTree properties, but I am completely lost on how to proceed. I’ve been digging around the forum for weeks and searching the Internet trying to find a clear example of current best practices around this, but everything I can find uses Parameters and is exposed to DAW automation. I also went through the whole “Creating Synthesizer Plug-Ins with C++ and JUCE” book and it doesn’t cover this either.

I have some rough idea how to do it, but I have encountered forum threads such as:
Very specific question about ValueTree thread safety - #6 by reuk
which make me think I have to be very careful about thread safety and asynchronicity. My problem is I am still new to C++ and JUCE and I just don’t know how to do this correctly/safely.

I learn by example so if I could find some code or a tutorial that shows me a good pattern, I can run with that. But I just keep finding forum threads that confuse me or make me concerned I’m going to cause data races and random crashes :frowning:

Any suggestions?

Here is a quick and dirty solution that I have used:

  • Create a dummy juce::AudioProcessor
  • Declare it as a private member of the main processor
  • Attach the APVTS (which contains non-automatable parameters) to the dummy processor

The constructor of the main processor may look like this (parametersNA and state constains non-automatable parameters):

PluginProcessor::PluginProcessor()
    : AudioProcessor(BusesProperties()
          .withInput("Input", juce::AudioChannelSet::stereo(), true)
          .withOutput("Output", juce::AudioChannelSet::stereo(), true)
          .withInput("Aux", juce::AudioChannelSet::stereo(), true)
      ), dummyProcessor(),
      parameters(*this, nullptr,
                 juce::Identifier("ZLEqualizerParameters"),
                 zlDSP::getParameterLayout()),
      parametersNA(dummyProcessor, nullptr,
                   juce::Identifier("ZLEqualizerParametersNA"),
                   zlState::getNAParameterLayout()),
      state(dummyProcessor, nullptr,
            juce::Identifier("ZLEqualizerState"),
            zlState::getStateParameterLayout())
2 Likes

Cool! Thanks zsliu98!! This is a very interesting idea. It is appealing I could use a consistent ParameterAttachment interface for everything. I will definitely be experimenting with this.

Something I want to figure out later is how to have 8 copies of the ~entire plugin state and allow for MIDI key-switching or automation to change between states (for example, like Sugar Bytes Effectrix). I bet additional APVTS data structures in dummy processors could work for this scenario too. When a new state is activated, I could copy one of these APVTS’s data into the main one.

PS - I starred your github repositories and will be studying your code. It’s good to find an open source JUCE project actively being maintained. I’ve looked through several JUCE projects’ code, but many haven’t been touched in years and are not even using the attachment system (because I guess it didn’t exist at the time).

Following up: I’ve been using a dummy processor and “hidden” apvts inside the dummy processor for the last month, and it works great in my project. Persistence with the two apvts’s works fine. I did it like this:

// initialization of the apvts:
apvts {mainProcessor, nullptr, "STATE", createParameterLayout()}

hiddenApvts {dummyProcessor, nullptr, "UA_STATE", createHiddenParameterLayout()}  // UA == "un-automatable"

// In the main AudioProcessor

void PluginProcessor::getStateInformation(juce::MemoryBlock& destData)
{
    auto xml { std::make_unique<juce::XmlElement>(pluginTag) };

    std::unique_ptr<juce::XmlElement> mainStateXML(apvts.copyState().createXml());
    xml->addChildElement(mainStateXML.release()); // added to <STATE> tag

    std::unique_ptr<juce::XmlElement> hiddenStateXML(hiddenApvts.copyState().createXml()); // added to <UA_STATE> tag
    xml->addChildElement(hiddenStateXML.release());

    copyXmlToBinary(*xml, destData);
}

void PluginProcessor::setStateInformation(const void* data, int sizeInBytes)
{
    std::unique_ptr<juce::XmlElement> xml { getXmlFromBinary(data, sizeInBytes) };

    if (xml.get() != nullptr && xml->hasTagName(pluginTag)) {
        if (auto* mainStateXML = xml->getChildByName(apvts.state.getType())) {
            apvts.replaceState(juce::ValueTree::fromXml(*mainStateXML));
        }
        if (auto* hiddenStateXML = xml->getChildByName(hiddenApvts.state.getType())) {
            hiddenApvts.replaceState(juce::ValueTree::fromXml(*hiddenStateXML));
        }
    }
}

I haven’t gotten around to implementing undo/redo yet, but I expect it will work fine. At least, I hope sharing one UndoManager between two apvts will not be an issue. I will report back if I run into any problems there.

1 Like

This works fine.

2 Likes