MetaSound Node Configuration API | Serialized Operator Data

Table of Contents

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:

  1. Create a custom MetaSound operator (TExecutableOperator)

  2. Define a vertex interface (FVertexInterface, TInputDataVertex, TOutputDataVertex)

  3. Register nodes (METASOUND_REGISTER_NODE, metadata, etc.)

  4. Use custom MetaSound data types (proxies, ReadRef/WriteRef)

This tutorial focuses on what changed in UE 5.7:

  1. What “Node Configuration” actually is at the document level

  2. How it enables persistent, per-node-instance state (serialized in the MetaSound asset)

  3. Why IOperatorData exists and why you should treat it as an Audio Thread payload

  4. How to integrate FMetaSoundFrontendNodeConfiguration into an existing custom node

  5. Why TNodeFacade is usually required for configuration-based operator data injection

  6. A 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:

  • IOperator instances do DSP work

  • Operators 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 ConfigString

  • Stored per node instance in the MetaSound asset

  • Exposed in the Details panel

  • Copied into an IOperatorData type 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<FExampleOperatorData>
	{
	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<const Metasound::IOperatorData> GetOperatorData() const override;

private:
	mutable TSharedPtr<Metasound::FExampleOperatorData> OperatorData;
};

				
			

In the .cpp:

				
					TSharedPtr<const Metasound::IOperatorData>
FExampleNodeConfiguration::GetOperatorData() const
{
	if (!OperatorData.IsValid())
	{
		OperatorData = MakeShared<Metasound::FExampleOperatorData>(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<FExampleOperator>;
}

				
			

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<IOperator> FExampleOperator::CreateOperator(
	const FBuildOperatorParams& InParams,
	FBuildResults& OutResults)
{
	const TSharedPtr<const IOperatorData> NodeData =
		InParams.Node.GetOperatorData();

	const FExampleOperatorData* Typed =
		CastOperatorData<const FExampleOperatorData>(NodeData.Get());

	if (!Typed)
	{
		return nullptr;
	}

	const TSharedPtr<const FExampleOperatorData> TypedShared =
		StaticCastSharedPtr<const FExampleOperatorData>(NodeData);

	return MakeUnique<FExampleOperator>(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<FExampleOperator>
{
private:
	TSharedPtr<const FExampleOperatorData> OperatorData;

//Constructor here()...etc
};

				
			

NodeConfiguration's OverrideDefaultInterface() function

Node Configuration has a second major capability:

				
					virtual TInstancedStruct<FMetasoundFrontendClassInterface>
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.

Rated 5 out of 5

Gamechanger plugin for notehighway rhythm games and music visualizers

Rated 5 out of 5

Add Text and 2D UMG widgets to Level Sequences