| name | valuetree |
| description | JUCE ValueTree and serialization patterns for Tracktion Engine plugins. Use when working with plugin state, CachedValue properties, ValueTree listeners, or state persistence/restoration. |
JUCE ValueTree & Serialization Patterns
ValueTree is the core data model in JUCE and Tracktion Engine. Every plugin, track, and edit stores its state as a ValueTree. This skill covers the patterns used throughout the MAGDA codebase.
ValueTree Basics
Creating and Populating
#include <juce_data_structures/juce_data_structures.h>
juce::ValueTree tree(juce::Identifier("MyPlugin"));
tree.setProperty("gain", 0.5f, nullptr);
tree.setProperty("name", "Lead", undoManager);
juce::ValueTree child(juce::Identifier("CHAIN"));
child.setProperty("index", 0, nullptr);
tree.addChild(child, -1, nullptr);
Reading Properties
float gain = tree.getProperty("gain", 1.0f);
juce::String name = tree.getProperty("name", "Default");
if (tree.hasProperty("gain")) { }
juce::ValueTree chainTree = tree.getChildWithName("CHAIN");
if (chainTree.isValid()) {
int idx = chainTree.getProperty("index");
}
Hierarchy Navigation
juce::ValueTree parent = tree.getParent();
for (int i = 0; i < tree.getNumChildren(); ++i) {
auto child = tree.getChild(i);
if (child.hasType("CHAIN")) {
}
}
for (auto child : tree) {
DBG(child.getType().toString());
}
for (int i = 0; i < tree.getNumChildren(); ++i) {
auto child = tree.getChild(i);
if (child.hasType("CHAIN") && (int)child.getProperty("index") == 2)
return child;
}
Identifier Constants
Always define Identifiers as static constants to avoid repeated string hashing:
Header (.h):
class MyPlugin : public te::Plugin {
static const juce::Identifier gainId;
static const juce::Identifier chainId;
static const juce::Identifier muteId;
};
Source (.cpp):
const juce::Identifier MyPlugin::gainId("gain");
const juce::Identifier MyPlugin::chainId("CHAIN");
const juce::Identifier MyPlugin::muteId("mute");
CachedValue
CachedValue<T> binds a C++ variable to a ValueTree property. It caches the value locally so reads are thread-safe (no locking), making it suitable for audio-thread reads.
Binding to a Property
class MyPlugin : public te::Plugin {
juce::CachedValue<float> level;
juce::CachedValue<float> pan;
juce::CachedValue<bool> mute;
juce::CachedValue<bool> solo;
MyPlugin(te::PluginCreationInfo info) : te::Plugin(info) {
level.referTo(state, "level", nullptr, 1.0f);
pan.referTo(state, "pan", nullptr, 0.0f);
mute.referTo(state, "mute", nullptr, false);
solo.referTo(state, "solo", nullptr, false);
}
};
Reading and Writing
float currentLevel = level.get();
float val = level;
level = 0.75f;
state.setProperty("level", 0.75f, nullptr);
Force Update After State Restore
After restorePluginStateFromValueTree() copies new values into the state tree, the CachedValues may still hold stale data. You must call forceUpdateOfCachedValue():
void restorePluginStateFromValueTree(const juce::ValueTree& v) override {
te::copyPropertiesToCachedValues(state, v, nullptr);
level.forceUpdateOfCachedValue();
pan.forceUpdateOfCachedValue();
mute.forceUpdateOfCachedValue();
solo.forceUpdateOfCachedValue();
}
Serialization Pattern in Tracktion Engine Plugins
Plugin state Member
Every te::Plugin has a state member which is a juce::ValueTree. This is the single source of truth for all plugin data.
Full Plugin Pattern
class MyPlugin : public te::Plugin {
public:
MyPlugin(te::PluginCreationInfo);
~MyPlugin() override;
static const char* xmlTypeName;
void restorePluginStateFromValueTree(const juce::ValueTree&) override;
juce::String getName() const override { return "My Plugin"; }
private:
static const juce::Identifier levelId;
static const juce::Identifier panId;
static const juce::Identifier chainTreeId;
juce::CachedValue<float> level;
juce::CachedValue<float> pan;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MyPlugin)
};
const juce::Identifier MyPlugin::levelId("level");
const juce::Identifier MyPlugin::panId("pan");
const juce::Identifier MyPlugin::chainTreeId("CHAIN");
MyPlugin::MyPlugin(te::PluginCreationInfo info)
: te::Plugin(info)
{
level.referTo(state, levelId, nullptr, 1.0f);
pan.referTo(state, panId, nullptr, 0.0f);
if (!state.getChildWithName(chainTreeId).isValid()) {
juce::ValueTree chainTree(chainTreeId);
chainTree.setProperty("index", 0, nullptr);
state.addChild(chainTree, -1, nullptr);
}
}
MyPlugin::~MyPlugin() {
notifyListenersOfDeletion();
}
void MyPlugin::restorePluginStateFromValueTree(const juce::ValueTree& v) {
te::copyPropertiesToCachedValues(state, v, nullptr);
level.forceUpdateOfCachedValue();
pan.forceUpdateOfCachedValue();
for (int i = state.getNumChildren(); --i >= 0;)
state.removeChild(i, nullptr);
for (auto child : v) {
state.addChild(child.createCopy(), -1, nullptr);
}
}
ValueTree::Listener
Implementing a Listener
class MyComponent : public juce::Component,
private juce::ValueTree::Listener
{
public:
MyComponent(juce::ValueTree stateToWatch)
: watchedState(stateToWatch)
{
watchedState.addListener(this);
}
~MyComponent() override {
watchedState.removeListener(this);
}
private:
void valueTreePropertyChanged(juce::ValueTree& tree,
const juce::Identifier& property) override
{
if (property == MyPlugin::levelId) {
triggerAsyncUpdate();
}
}
void valueTreeChildAdded(juce::ValueTree& parent,
juce::ValueTree& child) override
{
}
void valueTreeChildRemoved(juce::ValueTree& parent,
juce::ValueTree& child,
int index) override
{
}
juce::ValueTree watchedState;
};
Thread Safety Warning
Listeners fire on the thread that made the change. If a property is set from the audio thread, the listener callback runs on the audio thread. Never do UI work directly in a listener that might be called from the audio thread. Use juce::AsyncUpdater or juce::MessageManager::callAsync() to bounce to the message thread.
UndoManager Integration
state.setProperty("gain", newValue, &edit.getUndoManager());
state.addChild(child, -1, &edit.getUndoManager());
state.setProperty("gain", newValue, nullptr);
state.addChild(child, -1, nullptr);
auto* um = getUndoManager();
state.setProperty("gain", newValue, um);
Common Patterns in This Codebase
Chain Properties
Plugins with multiple chains (e.g., drum grid, multi-output) store per-chain state as child trees:
for (int i = 0; i < numChains; ++i) {
juce::ValueTree chainTree("CHAIN");
chainTree.setProperty("index", i, nullptr);
chainTree.setProperty("level", 1.0f, nullptr);
chainTree.setProperty("pan", 0.0f, nullptr);
chainTree.setProperty("mute", false, nullptr);
chainTree.setProperty("solo", false, nullptr);
state.addChild(chainTree, -1, nullptr);
}
for (int i = 0; i < state.getNumChildren(); ++i) {
auto child = state.getChild(i);
if (child.hasType("CHAIN")) {
float chainLevel = child.getProperty("level", 1.0f);
float chainPan = child.getProperty("pan", 0.0f);
bool chainMute = child.getProperty("mute", false);
}
}
CachedValue Arrays for Chains
When you need audio-thread-safe reads for multiple chains:
struct ChainState {
juce::CachedValue<float> level;
juce::CachedValue<float> pan;
juce::CachedValue<bool> mute;
juce::CachedValue<bool> solo;
void referTo(juce::ValueTree& chainTree) {
level.referTo(chainTree, "level", nullptr, 1.0f);
pan.referTo(chainTree, "pan", nullptr, 0.0f);
mute.referTo(chainTree, "mute", nullptr, false);
solo.referTo(chainTree, "solo", nullptr, false);
}
void forceUpdate() {
level.forceUpdateOfCachedValue();
pan.forceUpdateOfCachedValue();
mute.forceUpdateOfCachedValue();
solo.forceUpdateOfCachedValue();
}
};
std::vector<ChainState> chainStates;
Finding Child Trees by Type
juce::ValueTree findChainByIndex(const juce::ValueTree& parentState, int index) {
for (int i = 0; i < parentState.getNumChildren(); ++i) {
auto child = parentState.getChild(i);
if (child.hasType("CHAIN") && (int)child.getProperty("index") == index)
return child;
}
return {};
}
std::vector<juce::ValueTree> getChains(const juce::ValueTree& parentState) {
std::vector<juce::ValueTree> chains;
for (int i = 0; i < parentState.getNumChildren(); ++i) {
auto child = parentState.getChild(i);
if (child.hasType("CHAIN"))
chains.push_back(child);
}
return chains;
}
Common Pitfalls
- Forgetting
forceUpdateOfCachedValue() after restorePluginStateFromValueTree() — CachedValues will hold stale data
- Setting properties from the audio thread — Triggers listeners on the audio thread; use CachedValue for reads, avoid writes from audio thread when possible
- Not removing listeners in destructors — Dangling listener = crash
- Using string literals instead of Identifier constants — Creates a new Identifier each time, wastes CPU on hashing
- Passing
undoManager during initialization — Use nullptr for initial setup; only pass undoManager for user-driven changes
- Forgetting
isValid() checks — getChildWithName() returns an invalid ValueTree if not found; always check before use