What this tutorial covers
How to leverage the new Metasounds Node Configuration in UE 5.7 to create serialized/persistant metasound node data.
This article assumes you already know how to:
Create a custom MetaSound operator (
TExecutableOperator)Define a vertex interface (
FVertexInterface,TInputDataVertex,TOutputDataVertex)Register nodes (
METASOUND_REGISTER_NODE, metadata, etc.)Use custom MetaSound data types (proxies,
ReadRef/WriteRef)
This tutorial focuses on what changed in UE 5.7:
What “Node Configuration” actually is at the document level
How it enables persistent, per-node-instance state (serialized in the MetaSound asset)
Why
IOperatorDataexists and why you should treat it as an Audio Thread payloadHow to integrate
FMetaSoundFrontendNodeConfigurationinto an existing custom nodeWhy
TNodeFacadeis usually required for configuration-based operator data injectionA brief, bounded introduction to
OverrideDefaultInterface()and when it is appropriate
1. The Problem UE Node Configuration In UE 5.7 Solves
Before UE 5.7, a custom MetaSound node did not have a first-class way to store custom per-node-instance data inside the MetaSound document.
If you wanted “each node instance has its own serialized data”, there was no ready-made system to do that/
UE 5.7’s “Node Configuration” system exists to solve these issues in a way that is aligned with MetaSound’s architecture.
2. The NodeConfiguration Struct
There is a new struct you’ll have to inherit from to use this new API.
struct FMetaSoundFrontendNodeConfiguration
This is not an operator concept. It is a frontend/document/serialization concept.
MetaSound has two relevant layers:
2.1 Frontend / Document Layer (Serialized, Editor-facing)
This is where the MetaSound graph is stored as data:
Nodes
Vertex definitions
Per-node configuration (new in 5.7)
A Node Configuration is a USTRUCT that is serialized with the MetaSound asset and is associated with a specific node instance.
So you can have:
Node A (same class as Node B)
Node B
Each has a different configuration struct instance
Both serialize into the MetaSound document
This is the primary “persistent per node” capability we want.
2.2 Runtime / Operator Layer (DSP, Audio thread)
This is where MetaSound executes:
IOperatorinstances do DSP workOperators run on the Audio Render Thread
Operators are expected to be deterministic and efficient
Operators must not rely on garbage-collected objects or editor-only document data
Node Configuration lives in the frontend/document world. Operators run in runtime/DSP world. The bridge between these two worlds is the function:
GetOperatorData()
we’ll discuss it later.
3. The IOperatorData class
IOperatorData is a payload class you create so that your operator can consume node configuration safely. “Safely” as in thread safety. Its a copy of the data inside the NodeConfiguration basically.
Why copy data instead of using NodeConfiguration struct directly:
Operators run on the Audio Thread.
USTRUCTs and UObjects are overwhelmingly treated as Game Thread–owned data and may be:
modified by editor/game thread code,
subject to garbage collection rules,
not designed for lock-free reads from the audio thread,
large and complex, with internal allocations.
If an operator read from your configuration USTRUCT directly, you get two fundamental classes of problems:
A) Lifetime and ownership
The operator is running while the editor/game world might:
reload/modify the document,
rebuild the graph,
destroy objects due to GC.
Even if you think it won’t happen, MetaSound’s pipeline is explicitly designed so the operator doesn’t depend on those lifetimes.
B) Threading and data races
Even if lifetime was stable, direct access introduces cross-thread reads/writes:
Game thread modifies configuration (e.g., user changes Details panel value)
Audio thread reads the configuration concurrently
For simple POD types you can sometimes make that safe with atomics, but for types like FString, TArray, and complex structs, it becomes unsafe quickly unless you build a dedicated synchronization mechanism.
MetaSound’s design intent is:
Game-thread/Editor data (configuration) should be copied into a runtime payload designed for operators.
IOperatorData is that runtime payload.
The System in One Sentence
Node Configuration (USTRUCT, serialized)
→ produces Operator Data (IOperatorData, runtime)
→Operatator Data used by Operator (AudioThread DSP).
Practical Integration (Minimal Example)
We’ll build a minimal configuration:
A string property called
ConfigStringStored per node instance in the MetaSound asset
Exposed in the Details panel
Copied into an
IOperatorDatatype for operator consumption
1. Define an Operator Data type
MetaSound provides TOperatorData<T> which implements IOperatorData and type naming. You must provide a unique OperatorDataTypeName. The name has to be exactly OperatorDataTypeName or the template won’t compile. Other than that, you can add whatever you want to your “OperatorDataType”, the lame copy of the ConfigData.
namespace Metasound
{
class FExampleOperatorData final : public TOperatorData
{
public:
static const FLazyName OperatorDataTypeName;
explicit FExampleOperatorData(const FString& InConfigString)
: ConfigString(InConfigString)
{
}
FString ConfigString;
};
}
In the .cpp:
const FLazyName Metasound::FExampleOperatorData::OperatorDataTypeName =
TEXT("ExampleOperatorData");
Why this exists
This object is what the operator is allowed to “own” and read during DSP. It is not a USTRUCT; it is not GC-managed.
2 Create the Node Configuration USTRUCT
USTRUCT()
struct FExampleNodeConfiguration : public FMetaSoundFrontendNodeConfiguration
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, Category="General")
FString ConfigString;
virtual TSharedPtr GetOperatorData() const override;
private:
mutable TSharedPtr OperatorData;
};
In the .cpp:
TSharedPtr
FExampleNodeConfiguration::GetOperatorData() const
{
if (!OperatorData.IsValid())
{
OperatorData = MakeShared(ConfigString);
}
else
{
// Updates are reflected through the same shared object.
OperatorData->ConfigString = ConfigString;
}
return OperatorData;
}
GetOperatorData() function override
You can see that we’re basically creating an instance of our IOperatorData (FExampleOperatorData) and we simply copy our class members (ConfigString) to it. This is where we’re copying GT data to the AT owned object.
3. Update your FNodeFacade class
You’ll then need to update (replace) your FNodeFacade class. The reason being your FNodeFacade class needs a specific constructor in order for all of this to work. To avoid the headache, you can simply define your node facade like this:
namespace Metasound
{
using FExampleNode = TNodeFacade;
}
Why this works
TNodeFacade provides the constructors and wiring expected by the MetaSound node registry so that the node instance can carry configuration-produced operator data into the operator build pipeline.
4. Update The Register Node Macro To Include The Configuration
Replace your old Register Node macro with this
METASOUND_REGISTER_NODE_AND_CONFIGURATION(
Metasound::FExampleNode,
FExampleNodeConfiguration
);
Why this matters
Without this pairing, the node registry doesn’t know:
which configuration struct belongs to your node class
how to instantiate/store it per node instance
how to expose it in editor details
how to produce operator data from it
5. Use The Operator Data (config data) in your Operator
Your operator has a CreateOperator() static member that you’ve been using. It’s in this function where we’ll grab the serialized data from InParams.Node.GetOperatorData()
TUniquePtr FExampleOperator::CreateOperator(
const FBuildOperatorParams& InParams,
FBuildResults& OutResults)
{
const TSharedPtr NodeData =
InParams.Node.GetOperatorData();
const FExampleOperatorData* Typed =
CastOperatorData(NodeData.Get());
if (!Typed)
{
return nullptr;
}
const TSharedPtr TypedShared =
StaticCastSharedPtr(NodeData);
return MakeUnique(TypedShared, InParams);
}
Add the OperatorData As Constructor Argument and Store it.
Make sure your Operator class’s constructor has the OperatorData as an In Member so that you can store it.
class FExampleOperator : public TExecutableOperator
{
private:
TSharedPtr OperatorData;
//Constructor here()...etc
};
NodeConfiguration's OverrideDefaultInterface() function
Node Configuration has a second major capability:
virtual TInstancedStruct
OverrideDefaultInterface(const FMetasoundFrontendClass& InNodeClass) const;
This is not about passing data to the operator.
This is about changing the node’s pin schema based on configuration.
Use it when configuration affects wiring, for example:
variable number of inputs/outputs
optional sidechain pins
mode switches that add/remove pins
channel count selection that changes IO layout
It operates at the graph schema level rather than runtime DSP and deserves its own article.
GetOperatorData()controls operator behavior.OverrideDefaultInterface()controls the node interface.