My findings:
Who owns what?
Plugin Processor
- Owns the APVTS
- Owns the Internal State ValueTree
- Can write to both, but only via scheduling updates on the message thread. In the case of the Internal State ValueTree, we additionally update the atomic mirror which is a member variable member variable we use so that we read and write from the atomic instead, and schedule the value tree to catch up.
Plugin Editor
- Is a listener to the internal state value tree. So when the internal state value tree updates, get the listener callback or read directly using getRawParameterValue.
- We write to the param state directly in the audio processor, which will use param->getNormalisableRange().convertTo0to1(value) and call setValueNotifyingHost.
Who does what?
GUI thread → Will do two updates (Message thread)
- Updates the ValueTree on the audio processor using setProperty (safe, because GUI owns the ValueTree).
- Updates the atomic member variable we made to mirror the property on the audio processor immediately so we get fast realtime access to any properties.
Audio thread → Only updates atomics and schedules valuetree updates (Real-time audio callback)
- NEVER modifies the ValueTree directly.
- Updates the atomic member variable immediately
- Schedules an async message (via
MessageManager::callAsync
) to later update the ValueTree on the Messenge thread. See below for the code.
Internal Value Tree Updates
Make a setInternalBool method on the audio processor. It should update the atomic. If we are the message thread, update the value tree property. If we are the audio thread or any other thread, schedule the message thread to update it async.
APVTS Updates
The GUI is painted by the message thread, but the message thread has other tasks that are unrelated the graphics. The message thread exists without there being a GUI present and is just a general thread we use to make updates.
When the audio thread wants to change a param, we schedule it via the message thread and make sure the message thread calls setValueNotifyingHost. When the GUI thread wants to change a param, it should be able to do so without issue. This means you can make a generic setParamBool on the audio processor, and check if it’s the messenger thread. If it is, we just call setValueNotifyingHost, if not, we async tell messenger thread to do it. This means there might be some delay before it’s done. This means, as you guessed it, we also want to use an atomic paradigm, so that the audio thread always is able to get the immediate truth. This means that the value tree of the param may lag behind a bit, which means you’re GUI might lag behind a bit because the UI reads from the tree, but at least the DSP will always be up-to-date with the state of things.
setStateInformation And getStateInformation
This will always be called by the plugins Messenger Thread (GUI Thread), not the audio processor thread! This means that in setStateInformation after you have replaced the APVTS and Internal State, you need to call refreshAtomicsFromTree() to update the atomic member variables corresponding to each property of the internal state tree (this is not needed for the APVTS tree because we don’t use our own atomic mirrors).
Here’s the catch. Most hosts call getState on the UI thread, but Pro Tools, GarageBand and a few others use their own worker threads. They still don’t call from the realtime audio thread, so heap allocations are fine, but you can’t assume MessageManager::isThisTheMessageThread()
is true. So that means, when we get state, if we don’t want to clash with the GUI which might be writing to the tree, so we actually need to reconstructive the internal tree using the atomics.
ValueTree String Properties
We can’t safely put these inside an atomic the way we can with other primitive types. We must keep it guarded by a lock-free single-producer single-consumer queue (the GUI is producer, audio is consumer). Maybe someone can provide an example of this.
tldr:
- Processor writes on the Message thread, not the audio thread
- The internal ValueTree is the plugin’s true saved state and lives on the plugin processor. Atomics are runtime helpers for real-time audio. When we restore the serialized ValueTree by replacing the current tree (copyPropertiesAndChildrenFrom), we update the atomics which are member variables of the plugin processor, so they are also up to date.
- The APVTS lives on the processor as well, it is okay for both threads to read from. Writing is okay from the GUI (messenger thread), but if needed to be done in the audio processor (audio thread) we need to schedule the update to use the messenger thread. During that window, if the audio thread reads the value, it still sees the old atomic value stored in the APVTS since we are waiting for the message thread to update the tree and set it’s internal atomic for us to read. However, if you really really need it to be immediate, you can keep an atomic mirror of this param value of your own. I assume this is needed for serious plugins like mastering ones where the audio thread always needs a completely up to date truth value as it executes, and cannot even tolerate a few ms of a param to update.
- For APVTS parameters: Use
getRawParameterValue
, read directly from audio thread or GUI thread. For writing, if you’re the GUI thread it’s okay, however if you’re the audio thread, schedule it on the GUI thread.
- For internal ValueTree properties (non-parameters): You must maintain your own atomics manually as member variables in the audio processor and load from them. If you write to it, write to it and also schedule the messenger thread to update the value tree property.
Further information:
- We can’t start threads from the audio processor thread. The new keyword means we allocate memory on the heap, which is not realtime safe.
- In practice, many developers use
CachedValue
for GUI components (to avoid constantly querying ValueTree), which is optional but can be useful if you are reading the same property many times. I don’t yet know how to use this.
Coding Examples
Assume you have these member variables on the audio processor.
// Value tree for params
juce::AudioProcessorValueTreeState apvts;
// Value tree for internal state
juce::ValueTree internalState;
// Map from property ID -> atomic<bool>
std::unordered_map<juce::Identifier, std::atomic<bool>> internalBoolAtomics;
Internal State Setter & Getter
namespace InternalStateID
{
const juce::Identifier isMuteToggleActive {"isMuteToggleActive"};
}
void MyProcessor::setInternalBool(const juce::Identifier& propertyID, bool newValue)
{
// Update atomic
if (internalBoolAtomics.find(propertyID) != internalBoolAtomics.end())
internalBoolAtomics[propertyID].store(newValue);
// Update ValueTree
if (juce::MessageManager::isThisTheMessageThread())
{
internalState.setProperty(propertyID, newValue, nullptr);
}
else
{
juce::MessageManager::callAsync([this, propertyID, newValue]()
{
internalState.setProperty(propertyID, newValue, nullptr);
});
}
}
bool MyProcessor::getInternalBool(const juce::Identifier& propertyID) const
{
if (auto it = internalBoolAtomics.find(propertyID); it != internalBoolAtomics.end())
return it->second.load();
// If not found, return false by default (or you can jassertfalse)
return false;
}
Usage:
// Adding example
internalBoolAtomics.emplace(InternalStateID::isMuteToggleActive, false);
// Setting example
processor.setInternalBool(InternalStateID::isMuteToggleActive, true);
// Getting example
bool muteIsActive = processor.getInternalBool(InternalStateID::isMuteToggleActive);
Param State Setter & Getter
namespace ParamStateID
{
const juce::Identifier isMuteToggleActive { "isMuteToggleActive" };
}
void MyProcessor::setParamBool(const juce::Identifier& paramID, bool newValue)
{
if (auto* param = dynamic_cast<juce::AudioParameterBool*>(apvts.getParameter(paramID.toString())))
{
const float normalisedValue = param->getNormalisableRange().convertTo0to1(newValue ? 1.0f : 0.0f);
if (juce::MessageManager::isThisTheMessageThread())
{
param->setValueNotifyingHost(normalisedValue);
}
else
{
juce::MessageManager::callAsync([param, normalisedValue]()
{
param->setValueNotifyingHost(normalisedValue);
});
}
}
else
{
jassertfalse; // ID not found or wrong type
}
}
bool MyProcessor::getParamBool(const juce::Identifier& paramID) const
{
if (auto* atomicParam = apvts.getRawParameterValue(paramID.toString()))
{
return atomicParam->load() >= 0.5f;
}
jassertfalse; // ID not found
return false;
}
Usage:
// Setting a param
processor.setParamBool(ParamStateID::isMuteToggleActive, true);
// Getting a parameter
bool muteParamIsActive = processor.getParamBool(ParamStateID::isMuteToggleActive);
The way to set the plugin state
void MyProcessor::setStateInformation (const void* data, int sizeInBytes)
{
auto xml = parseXml(data, sizeInBytes);
if (xml != nullptr)
{
auto newPluginState = juce::ValueTree::fromXml(*xml);
if (newPluginState.isValid())
{
auto newAPVTSstate = newPluginState.getChildWithName("APVTS");
auto newInternalState = newPluginState.getChildWithName("InternalState");
// Refresh atomics immediately (safe, even on worker thread)
refreshAtomicsFromTree(newInternalState);
// Schedule ValueTree replacements safely on the message thread
juce::MessageManager::callAsync([this, apvtsStateCopy = newAPVTSstate, internalStateCopy = newInternalState]() mutable
{
if (apvtsStateCopy.isValid())
apvts.replaceState(apvtsStateCopy);
if (internalStateCopy.isValid())
internalState.copyPropertiesAndChildrenFrom(internalStateCopy, nullptr);
});
}
}
}
More problems?
But if plugin editor can modify the value tree, and some other worker thread in a daw like GarageBand can come along and call getStateInformation, can’t we run into a memory problem? I think so, imagine the GUI thread is writing to it while this worker thread tried to get a copy. So we re-make a tree from the atomics.
void PluginProcessor::getStateInformation(juce::MemoryBlock& destData)
{
juce::ValueTree tree ("InternalState");
// Add the
tree.setProperty(InternalStateID::isMuteToggleActive, getInternalBool(InternalStateID::isMuteToggleActive), nullptr);
// etc. for all internal properties
// Add the apvts directly here since it's safe to read
// ....
// Turn into xml
// ....
// If valid, convert the XML into a binary blob for the host.
if (stateXml != nullptr){
copyXmlToBinary(*stateXml, destData);
}
}
So this is a rough sketch of what I believe Juce is missing. A realtime thread safe value tree without locks
InternalStateManager.h
#pragma once
#include <JuceHeader.h>
#include <unordered_map>
class InternalStateManager
{
public:
InternalStateManager(juce::ValueTree initialTree);
// Registration API
void registerBool(const juce::Identifier& propertyID, bool defaultValue);
void registerFloat(const juce::Identifier& propertyID, float defaultValue);
void registerInt(const juce::Identifier& propertyID, int defaultValue);
// Write API
void setBool(const juce::Identifier& propertyID, bool newValue);
void setFloat(const juce::Identifier& propertyID, float newValue);
void setInt(const juce::Identifier& propertyID, int newValue);
// Read API
bool getBool(const juce::Identifier& propertyID) const;
float getFloat(const juce::Identifier& propertyID) const;
int getInt(const juce::Identifier& propertyID) const;
// Saving and restoring plugin state
juce::ValueTree createSnapshotTree() const;
void refreshFromTree(const juce::ValueTree& tree);
// Access underlying ValueTree if needed (e.g., listeners)
juce::ValueTree& getInternalStateTree();
private:
juce::ValueTree internalState;
std::unordered_map<juce::Identifier, std::atomic<bool>> boolAtomics;
std::unordered_map<juce::Identifier, std::atomic<int>> intAtomics;
std::unordered_map<juce::Identifier, std::atomic<float>> floatAtomics;
// Internal async updater for ValueTree
template <typename ValueType>
void updateValueTreeAsync(const juce::Identifier& propertyID, ValueType newValue);
};
InternalStateManager.cpp
#include "InternalStateManager.h"
InternalStateManager::InternalStateManager(juce::ValueTree initialTree)
: internalState(initialTree)
{
}
// --- Registration ---
void InternalStateManager::registerBool(const juce::Identifier& propertyID, bool defaultValue)
{
boolAtomics.emplace(propertyID, defaultValue);
if (!internalState.hasProperty(propertyID))
internalState.setProperty(propertyID, defaultValue, nullptr);
}
void InternalStateManager::registerFloat(const juce::Identifier& propertyID, float defaultValue)
{
floatAtomics.emplace(propertyID, defaultValue);
if (!internalState.hasProperty(propertyID))
internalState.setProperty(propertyID, defaultValue, nullptr);
}
void InternalStateManager::registerInt(const juce::Identifier& propertyID, int defaultValue)
{
intAtomics.emplace(propertyID, defaultValue);
if (!internalState.hasProperty(propertyID))
internalState.setProperty(propertyID, defaultValue, nullptr);
}
// --- Write API ---
void InternalStateManager::setBool(const juce::Identifier& propertyID, bool newValue)
{
if (auto it = boolAtomics.find(propertyID); it != boolAtomics.end())
it->second.store(newValue);
else
jassertfalse;
updateValueTreeAsync(propertyID, newValue);
}
void InternalStateManager::setFloat(const juce::Identifier& propertyID, float newValue)
{
if (auto it = floatAtomics.find(propertyID); it != floatAtomics.end())
it->second.store(newValue);
else
jassertfalse;
updateValueTreeAsync(propertyID, newValue);
}
void InternalStateManager::setInt(const juce::Identifier& propertyID, int newValue)
{
if (auto it = intAtomics.find(propertyID); it != intAtomics.end())
it->second.store(newValue);
else
jassertfalse;
updateValueTreeAsync(propertyID, newValue);
}
// --- Read API ---
bool InternalStateManager::getBool(const juce::Identifier& propertyID) const
{
if (auto it = boolAtomics.find(propertyID); it != boolAtomics.end())
return it->second.load();
jassertfalse;
return false;
}
float InternalStateManager::getFloat(const juce::Identifier& propertyID) const
{
if (auto it = floatAtomics.find(propertyID); it != floatAtomics.end())
return it->second.load();
jassertfalse;
return 0.0f;
}
int InternalStateManager::getInt(const juce::Identifier& propertyID) const
{
if (auto it = intAtomics.find(propertyID); it != intAtomics.end())
return it->second.load();
jassertfalse;
return 0;
}
// --- Saving/Loading State ---
juce::ValueTree InternalStateManager::createSnapshotTree() const
{
juce::ValueTree snapshot(internalState.getType());
for (const auto& [id, value] : boolAtomics)
snapshot.setProperty(id, value.load(), nullptr);
for (const auto& [id, value] : intAtomics)
snapshot.setProperty(id, value.load(), nullptr);
for (const auto& [id, value] : floatAtomics)
snapshot.setProperty(id, value.load(), nullptr);
return snapshot;
}
void InternalStateManager::refreshFromTree(const juce::ValueTree& tree)
{
for (const auto& [id, _] : boolAtomics)
boolAtomics[id].store(static_cast<bool>(tree.getProperty(id, false)));
for (const auto& [id, _] : intAtomics)
intAtomics[id].store(static_cast<int>(tree.getProperty(id, 0)));
for (const auto& [id, _] : floatAtomics)
floatAtomics[id].store(static_cast<float>(tree.getProperty(id, 0.0f)));
}
// --- Access Internal ValueTree ---
juce::ValueTree& InternalStateManager::getInternalStateTree()
{
return internalState;
}
// --- Async ValueTree Update Helper ---
template <typename ValueType>
void InternalStateManager::updateValueTreeAsync(const juce::Identifier& propertyID, ValueType newValue)
{
if (juce::MessageManager::isThisTheMessageThread())
{
internalState.setProperty(propertyID, newValue, nullptr);
}
else
{
juce::MessageManager::callAsync([tree = internalState, propertyID, newValue]() mutable
{
tree.setProperty(propertyID, newValue, nullptr);
});
}
}
// Explicit template instantiations (needed because updateValueTreeAsync is a template inside a .cpp file)
template void InternalStateManager::updateValueTreeAsync<bool>(const juce::Identifier&, bool);
template void InternalStateManager::updateValueTreeAsync<int>(const juce::Identifier&, int);
template void InternalStateManager::updateValueTreeAsync<float>(const juce::Identifier&, float);