🧠 Brains complement: IF an unreal-engine-skills-manager tool (external MCP) exists in this session, call it with {action: "load", skill: "audio-and-metasounds"} for UE domain knowledge on this topic — correct APIs, architecture, best practices — and treat it as the rubric for any review / "best practices" question. If no such tool is available (e.g. running under Claude Code or Codex without that MCP), skip this line entirely and proceed with this skill alone — do NOT attempt the call.
MetaSound Service Skill
Use this skill to create and edit MetaSound Source assets via Python.
import unreal
ms = unreal.MetaSoundService()
Key Concepts
- MetaSound Source — a procedural audio asset (
UMetaSoundSource) defined by a node graph. It replaces SoundCue for runtime-parameterisable sounds.
- Node — a DSP processing block (Sine oscillator, Gain, Delay, etc.). Nodes have named input pins and output pins with associated DataTypes.
- NodeId — a GUID string returned by
add_node. Pass it to connect, remove, set_default, etc.
- Graph I/O —
add_graph_input / add_graph_output expose values at the asset level (settable at runtime via Set Float Parameter, etc.).
- Standard interface — every Source created by this service comes pre-wired with:
On Play (Trigger output) — fires when the sound starts
On Finished (Trigger input) — call to stop the sound
Audio:0 (Audio input on the graph output node) — connect your audio signal here
Workflow
1 — Discover available nodes
# List all nodes whose class name or display name contains "Sine"
nodes = ms.list_available_nodes("Sine")
for n in nodes:
print(n.full_class_name, " inputs:", n.inputs, " outputs:", n.outputs)
2 — Create a MetaSound
r = ms.create_meta_sound("/Game/Audio", "MS_SineLoop", "Mono")
asset_path = r.asset_path # "/Game/Audio/MS_SineLoop"
3 — Find the built-in interface node IDs
all_nodes = ms.list_nodes(asset_path)
for n in all_nodes:
print(n.node_id, n.node_title, n.inputs, n.outputs)
# IMPORTANT: A MetaSound Source has MULTIPLE nodes with title "Output" —
# one per interface pin group (e.g. "On Finished" Trigger, "Out Mono" Audio).
# You MUST filter by the node whose inputs contain an Audio-type pin.
# Pin strings are "VertexName:TypeName" — the TypeName suffix after the last colon.
audio_out_node = next(
n for n in all_nodes
if n.node_title == "Output" and any(p.endswith(":Audio") for p in n.inputs)
)
audio_out_id = audio_out_node.node_id
# Drop the last :TypeName suffix to get the raw vertex name for connect_nodes
audio_in_pin = ":".join(audio_out_node.inputs[0].split(":")[:-1]) # "UE.OutputFormat.Mono.Audio:0"
# Input node — filter by class_name "Input.Trigger" to avoid matching graph input nodes
# (graph inputs also appear as "Input" nodes but with class "Input.Float", "Input.Bool", etc.)
input_node = next(n for n in all_nodes if n.class_name == "Input.Trigger")
input_node_id = input_node.node_id # "UE.Source.OnPlay"
4 — Add a node
# add_node(asset_path, namespace, name, variant="", major_version=1, pos_x=0, pos_y=0)
# Use list_available_nodes() to discover the correct namespace/name/variant for any node
r2 = ms.add_node(asset_path, "UE", "Sine", "Audio", 1, -200.0, 0.0)
sine_id = r2.node_id # GUID string
5 — Set a node input default
# Set the Sine frequency to 880 Hz
# Use get_node_pins() to confirm exact input pin names before setting
ms.set_node_input_default(asset_path, sine_id, "Frequency", "880.0", "Float")
# Read it back (issue #460):
ms.get_node_input_default(asset_path, sine_id, "Frequency") # -> "880.000000"
# All inputs at once: name / data_type / default_value / is_connected
for v in ms.get_node_input_values(asset_path, sine_id):
print(v.name, v.data_type, v.default_value, v.is_connected)
6 — Connect nodes
# connect_nodes(asset_path, from_node_id, output_name, to_node_id, input_name)
# Sine output pin is "Audio"; AudioOut input pin is "UE.OutputFormat.Mono.Audio:0"
ms.connect_nodes(asset_path, sine_id, "Audio", audio_out_id, audio_in_pin)
# Read the wiring back (issue #460) — every edge touching a node, both directions:
for c in ms.get_node_connections(asset_path, sine_id):
print(f"{c.from_node_id}.{c.from_output} -> {c.to_node_id}.{c.to_input} [{c.data_type}]")
7 — Save
ms.save_meta_sound(asset_path)
Method Reference
Lifecycle
| Method |
Description |
create_meta_sound(package_path, asset_name, output_format="Mono") |
Create a new MetaSound Source asset. Returns FMetaSoundResult with asset_path. |
delete_meta_sound(asset_path) |
Delete a MetaSound asset. |
get_meta_sound_info(asset_path) |
Return FMetaSoundInfo (node count, output format, graph I/O names). |
save_meta_sound(asset_path) |
Save after edits. Always call after making graph changes. |
Node Discovery
| Method |
Description |
list_available_nodes(search_filter="") |
List all registered External/DSP node classes. Returns TArray<FMetaSoundNodeClassInfo>. Each entry has full_class_name, namespace, name, variant, inputs, outputs, display_name. |
Node Management
| Method |
Returns |
Description |
add_node(asset_path, namespace, name, variant="", major_version=1, pos_x=0, pos_y=0) |
FMetaSoundResult |
Add a node. node_id on result is the GUID string. |
remove_node(asset_path, node_id) |
FMetaSoundResult |
Remove node and all its edges. |
list_nodes(asset_path) |
TArray<FMetaSoundNodeInfo> |
List all nodes in the graph. |
get_node_pins(asset_path, node_id) |
FMetaSoundNodeInfo |
Return pin info for a single node. |
Connections
| Method |
Returns |
Description |
connect_nodes(asset_path, from_node_id, output_name, to_node_id, input_name) |
FMetaSoundResult |
Connect an output pin to an input pin. Check r.success. |
disconnect_pin(asset_path, node_id, input_name) |
FMetaSoundResult |
Remove the connection going into an input pin. |
Graph I/O
| Method |
Returns |
Description |
add_graph_input(asset_path, input_name, data_type, default_value="") |
FMetaSoundResult |
Add a named input exposed as a runtime parameter. Appears as an Input.<Type> node in the graph. |
add_graph_output(asset_path, output_name, data_type) |
FMetaSoundResult |
Add a named output. |
remove_graph_input(asset_path, input_name) |
FMetaSoundResult |
Remove a graph input. |
remove_graph_output(asset_path, output_name) |
FMetaSoundResult |
Remove a graph output. |
Node Configuration
| Method |
Returns |
Description |
set_node_input_default(asset_path, node_id, input_name, value, data_type) |
FMetaSoundResult |
Set a literal default on a node input. data_type: "Float", "Int32", "Bool", "String", "WaveAsset". |
set_node_location(asset_path, node_id, pos_x, pos_y) |
FMetaSoundResult |
Update editor position. |
Common DataTypes
| Type name |
Description |
Float |
32-bit float (frequency, gain, time) |
Int32 |
Integer |
Bool |
Boolean |
String |
Text string |
Audio |
Audio signal (mono channel) |
Trigger |
Impulse / event signal |
Time |
Duration in seconds (use Float in most cases) |
WaveAsset |
Reference to a SoundWave asset |
Complete Example — 880 Hz Sine Tone
import unreal
ms = unreal.MetaSoundService()
# Create asset
r = ms.create_meta_sound("/Game/Audio", "MS_Test880Hz", "Mono")
ap = r.asset_path
# Find the AudioOut node — there are multiple nodes titled "Output" (one per interface
# pin group). Filter for the one whose inputs contain an Audio-type pin.
nodes = ms.list_nodes(ap)
audio_out_node = next(
n for n in nodes
if n.node_title == "Output" and any(p.endswith(":Audio") for p in n.inputs)
)
audio_out_id = audio_out_node.node_id
# Pin strings are "VertexName:TypeName" — drop only the last :TypeName to get the vertex name
audio_in_pin = ":".join(audio_out_node.inputs[0].split(":")[:-1]) # "UE.OutputFormat.Mono.Audio:0"
# Add Sine oscillator (use list_available_nodes("Sine") to discover namespace/name/variant)
r2 = ms.add_node(ap, "UE", "Sine", "Audio", 1, -300.0, 0.0)
sine_id = r2.node_id
# Set frequency to 880 Hz (use get_node_pins() to confirm exact pin names)
ms.set_node_input_default(ap, sine_id, "Frequency", "880.0", "Float")
# Connect Sine "Audio" output to the AudioOut sink pin
ms.connect_nodes(ap, sine_id, "Audio", audio_out_id, audio_in_pin)
# Save
ms.save_meta_sound(ap)
print("Done:", ap)
Complete Example — One-Shot SoundWave (Gunshot)
import unreal
ms = unreal.MetaSoundService()
# Create a Mono MetaSound Source
r = ms.create_meta_sound("/Game/Audio", "MS_Gunshot", "Mono")
ap = r.asset_path
# Find interface nodes
nodes = ms.list_nodes(ap)
# Input node — has "On Play" Trigger output
input_node = next(n for n in nodes if n.node_title == "Input")
input_node_id = input_node.node_id
# Audio Output node — filter for the one with an Audio-type input
audio_out_node = next(
n for n in nodes
if n.node_title == "Output" and any(p.endswith(":Audio") for p in n.inputs)
)
audio_out_id = audio_out_node.node_id
audio_in_pin = ":".join(audio_out_node.inputs[0].split(":")[:-1])
# Add Wave Player (Mono) node
# Pin names (no need to call get_node_pins — see Known Node Pins below):
# Inputs: "Play" (Trigger), "Stop" (Trigger), "Wave Asset" (WaveAsset), "Loop" (Bool)
# Outputs: "Out Mono" (Audio), "On Play" (Trigger), "On Finished" (Trigger)
wp = ms.add_node(ap, "UE", "Wave Player", "Mono", 1, -300.0, 0.0)
wp_id = wp.node_id
# Set the wave asset
ms.set_node_input_default(ap, wp_id, "Wave Asset", "/Game/Audio/SW_Gunshot_01", "WaveAsset")
# Wire On Play (Input) → Play (WavePlayer)
# CRITICAL: use the vertex name "UE.Source.OnPlay" — NOT the display name "On Play"
r = ms.connect_nodes(ap, input_node_id, "UE.Source.OnPlay", wp_id, "Play")
if not r.success: raise RuntimeError(f"connect On Play→Play failed: {r.message}")
# Wire Out Mono (WavePlayer) → audio sink (Output)
r = ms.connect_nodes(ap, wp_id, "Out Mono", audio_out_id, audio_in_pin)
if not r.success: raise RuntimeError(f"connect Out Mono→Output failed: {r.message}")
# Save
ms.save_meta_sound(ap)
print("Done:", ap)
Return Type Attribute Reference
Use these exact attribute names — guessing leads to AttributeError.
FMetaSoundResult — returned by most mutating methods
| Attribute |
Type |
Example |
success |
bool |
True |
message |
str |
"Connected A.Audio -> B.UE.OutputFormat.Mono.Audio:0" |
asset_path |
str |
"/Game/Audio/MS_Test" |
node_id |
str (GUID) |
"A1B2C3D4-..." (populated by add_node only) |
r = ms.connect_nodes(ap, from_id, "Audio", to_id, pin)
if not r.success:
raise RuntimeError(r.message)
# NOT r.b_success — that attribute does not exist
FMetaSoundInfo — returned by get_meta_sound_info()
| Attribute |
Type |
Example |
asset_path |
str |
"/Game/Audio/MS_Test_Blip" |
asset_name |
str |
"MS_Test_Blip" |
output_format |
str |
"Mono" |
node_count |
int |
4 |
graph_inputs |
list[str] |
[] |
graph_outputs |
list[str] |
[] |
info = svc.get_meta_sound_info(asset_path)
print(info.node_count, info.output_format)
# NOT info.success, info.b_success, info.message
FMetaSoundNodeInfo — returned by list_nodes() and get_node_pins()
| Attribute |
Type |
Example |
node_id |
str (GUID) |
"A1B2C3D4-..." |
node_title |
str |
"Wave Player" |
class_name |
str |
"UE.Wave Player.Mono" |
inputs |
list[str] |
["Play:Trigger", "Wave Asset:WaveAsset"] |
outputs |
list[str] |
["Out Mono:Audio", "On Finished:Trigger"] |
nodes = ms.list_nodes(asset_path)
for n in nodes:
print(n.node_id, n.node_title, n.class_name)
# NOT n.node_class, n.node_class_name, n.name
FMetaSoundNodeClassInfo — returned by list_available_nodes()
| Attribute |
Type |
Example |
full_class_name |
str |
"UE.Wave Player.Mono" |
namespace |
str |
"UE" |
name |
str |
"Wave Player" |
variant |
str |
"Mono" |
display_name |
str |
"Wave Player (Mono)" |
inputs |
list[str] |
["Play:Trigger", ...] |
outputs |
list[str] |
["Out Mono:Audio", ...] |
nodes = ms.list_available_nodes("Wave Player")
for n in nodes:
print(n.name, n.variant, n.full_class_name)
# NOT n.node_name, n.node_class, n.class_name
Known Node Pins
Use these instead of calling get_node_pins on freshly-added nodes (can time out).
Wave Player (UE.Wave Player.Mono)
| Direction |
Pin |
Type |
| Input |
Play |
Trigger |
| Input |
Stop |
Trigger |
| Input |
Wave Asset |
WaveAsset |
| Input |
Loop |
Bool |
| Input |
Pitch Shift |
Float |
| Input |
Start Time |
Time |
| Input |
Loop Start |
Time |
| Input |
Loop Duration |
Time |
| Input |
Maintain Audio Sync |
Bool |
| Output |
Out Mono |
Audio |
| Output |
On Play |
Trigger |
| Output |
On Finished |
Trigger |
| Output |
On Nearly Finished |
Trigger |
| Output |
On Looped |
Trigger |
| Output |
On Cue Point |
Trigger |
| Output |
Cue Point ID |
Int32 |
| Output |
Cue Point Label |
String |
| Output |
Loop Percent |
Float |
| Output |
Playback Location |
Float |
| Output |
Playback Time |
Time |
Sine (UE.Sine.Audio)
| Direction |
Pin |
Type |
| Input |
Frequency |
Float |
| Input |
Modulation |
Audio |
| Input |
Enabled |
Bool |
| Input |
Bi Polar |
Bool |
| Input |
Sync |
Trigger |
| Input |
Phase Offset |
Float |
| Input |
Glide |
Float |
| Input |
Type |
Enum:SineGenerationType |
| Output |
Audio |
Audio |
Standard Interface Nodes
CRITICAL: Interface node pins use namespaced vertex names. The display name shown
in the editor is NOT the vertex name. Always use the vertex names below in connect_nodes.
| Node title |
Vertex name (EXACT string for connect_nodes) |
Type |
Direction |
Input |
UE.Source.OnPlay |
Trigger |
Output |
Output (Trigger) |
UE.Source.OneShot.OnFinished |
Trigger |
Input |
Output (Audio, Mono) |
UE.OutputFormat.Mono.Audio:0 |
Audio |
Input |
Output (Audio, Stereo Left) |
UE.OutputFormat.Stereo.Audio:0 |
Audio |
Input |
Output (Audio, Stereo Right) |
UE.OutputFormat.Stereo.Audio:1 |
Audio |
Input |
Output (Audio, Quad 0–3) |
UE.OutputFormat.Quad.Audio:0 … :3 |
Audio |
Input |
The audio output format depends on the output_format passed to create_meta_sound
("Mono" / "Stereo" / "Quad"). A Stereo source has two Output.Audio nodes
(Left :0, Right :1); a Quad source has four. Mono has one.
Connecting to a Stereo (or Quad) output
A Mono asset has a single audio sink, so next(...) is fine. Stereo/Quad assets have one
audio output node per channel — grab them all and feed each one, or channels will be silent.
# Collect every audio sink node (works for Mono, Stereo, and Quad)
nodes = ms.list_nodes(ap)
audio_out_nodes = [
n for n in nodes
if n.node_title == "Output" and any(p.endswith(":Audio") for p in n.inputs)
]
# Sort by channel index so [0]=Left, [1]=Right (the vertex name ends with ":<index>:Audio")
audio_out_nodes.sort(key=lambda n: n.inputs[0])
# Mono: one node. Stereo: two (Left, Right). Quad: four.
# Example — send a mono signal (e.g. a Mixer "Out") to BOTH stereo channels:
for out_node in audio_out_nodes:
sink_pin = out_node.inputs[0] # e.g. "UE.OutputFormat.Stereo.Audio:0:Audio"
r = ms.connect_nodes(ap, source_id, "Out", out_node.node_id, sink_pin)
if not r.success: raise RuntimeError(r.message)
For a true stereo image, drive Left and Right from different signals (e.g. two oscillators,
or a stereo Audio Mixer (Stereo, N) whose Out L / Out R feed the two sinks). To get a
stereo mixer, search list_available_nodes("Mixer") for a (Stereo, …) variant — the
(Mono, N) mixer has a single Out pin and only fills one channel on its own.
connect_nodes accepts the full "VertexName:TypeName" pin string (it strips the trailing
:TypeName), so you can pass n.inputs[0] directly without manual splitting.
Notes
- Save after every batch of edits, not after each individual operation, to avoid excessive disk I/O.
list_available_nodes("") returns all registered node classes (~400+). Use a filter like "Sine", "Delay", "Gain" to narrow the results.
- Node pin names for standard nodes use UE display names (e.g.
"In Frequency", "Out", "Audio:0"). Use get_node_pins() to confirm exact names.
- A MetaSound Source has multiple nodes with
node_title == "Output" — one per interface pin group (e.g. "On Finished" Trigger, "Out Mono" Audio). To find the audio sink node, filter for the Output node whose inputs contain an Audio-type pin: next(n for n in nodes if n.node_title == "Output" and any(p.endswith(":Audio") for p in n.inputs)). Pin strings from list_nodes are "VertexName:TypeName" — you can pass them directly to connect_nodes, which now automatically strips the trailing :TypeName suffix. Manual stripping (":".join(pin.split(":")[:-1])) still works but is no longer required.
- Node namespace/name/variant values differ from what the MetaSound editor displays. Always call
list_available_nodes("keyword") to discover the correct values; do not guess.
- MetaSound Sources do not support SoundCue-style
SoundNodeWavePlayer — use the WavePlayer MetaSound node instead (discover via list_available_nodes("Wave Player")).
- Graph inputs appear as
Input nodes in the graph (class_name == "Input.Float", "Input.Bool", etc.). When filtering for the standard interface Input node (the one that carries On Play), always match by class_name == "Input.Trigger" — not by node_title == "Input" alone, which will also match graph input nodes.
- AudioMixer pins interleave audio and gain:
Audio Mixer (Mono, 2) has pins ["In 0:Audio", "Gain 0:Float", "In 1:Audio", "Gain 1:Float"]. Never index by position — always filter for :Audio typed pins when connecting audio signals: audio_ins = [p.split(":")[0] for p in mixer_node.inputs if p.endswith(":Audio")].
- Stereo MetaSound Sources have TWO audio output sinks, one per channel. A Stereo source's audio output pins are
UE.OutputFormat.Stereo.Audio:0 (Left) and UE.OutputFormat.Stereo.Audio:1 (Right) — exposed as two separate Output.Audio nodes, not one. (Quad has four: :0–:3.) You must feed both channels or one side will be silent. The single-next() audio-sink finder used for Mono only grabs one channel — for Stereo, collect every audio output node. See Connecting to a Stereo/Quad output below.
Sample scripts (run via execute_python_code)
scripts/create_metasound.txt — create a MetaSound Source and list available node types.
1---2name: metasounds3description: Create and modify MetaSound Source assets — add/connect nodes, wire pins, set input defaults, and play procedurally (MetaSoundService). Use when the user asks to create a MetaSound, build or edit a MetaSound graph, add operator/input/output nodes, or generate procedural audio.4---56> 🧠 **Brains complement:** IF an `unreal-engine-skills-manager` tool (external MCP) exists in this session, call it with `{action: "load", skill: "audio-and-metasounds"}` for UE domain knowledge on this topic — correct APIs, architecture, best practices — and treat it as the rubric for any review / "best practices" question. If no such tool is available (e.g. running under Claude Code or Codex without that MCP), skip this line entirely and proceed with this skill alone — do NOT attempt the call.78# MetaSound Service Skill910Use this skill to create and edit MetaSound Source assets via Python.1112```python13import unreal14ms = unreal.MetaSoundService()15```1617## Key Concepts1819- **MetaSound Source** — a procedural audio asset (`UMetaSoundSource`) defined by a node graph. It replaces SoundCue for runtime-parameterisable sounds.20- **Node** — a DSP processing block (Sine oscillator, Gain, Delay, etc.). Nodes have named **input pins** and **output pins** with associated **DataTypes**.21- **NodeId** — a GUID string returned by `add_node`. Pass it to connect, remove, set_default, etc.22- **Graph I/O** — `add_graph_input` / `add_graph_output` expose values at the asset level (settable at runtime via `Set Float Parameter`, etc.).23- **Standard interface** — every Source created by this service comes pre-wired with:24 - `On Play` (Trigger output) — fires when the sound starts25 - `On Finished` (Trigger input) — call to stop the sound26 - `Audio:0` (Audio input on the graph output node) — connect your audio signal here2728---2930## Workflow3132### 1 — Discover available nodes3334```python35# List all nodes whose class name or display name contains "Sine"36nodes = ms.list_available_nodes("Sine")37for n in nodes:38 print(n.full_class_name, " inputs:", n.inputs, " outputs:", n.outputs)39```4041### 2 — Create a MetaSound4243```python44r = ms.create_meta_sound("/Game/Audio", "MS_SineLoop", "Mono")45asset_path = r.asset_path # "/Game/Audio/MS_SineLoop"46```4748### 3 — Find the built-in interface node IDs4950```python51all_nodes = ms.list_nodes(asset_path)52for n in all_nodes:53 print(n.node_id, n.node_title, n.inputs, n.outputs)5455# IMPORTANT: A MetaSound Source has MULTIPLE nodes with title "Output" —56# one per interface pin group (e.g. "On Finished" Trigger, "Out Mono" Audio).57# You MUST filter by the node whose inputs contain an Audio-type pin.58# Pin strings are "VertexName:TypeName" — the TypeName suffix after the last colon.59audio_out_node = next(60 n for n in all_nodes61 if n.node_title == "Output" and any(p.endswith(":Audio") for p in n.inputs)62)63audio_out_id = audio_out_node.node_id64# Drop the last :TypeName suffix to get the raw vertex name for connect_nodes65audio_in_pin = ":".join(audio_out_node.inputs[0].split(":")[:-1]) # "UE.OutputFormat.Mono.Audio:0"6667# Input node — filter by class_name "Input.Trigger" to avoid matching graph input nodes68# (graph inputs also appear as "Input" nodes but with class "Input.Float", "Input.Bool", etc.)69input_node = next(n for n in all_nodes if n.class_name == "Input.Trigger")70input_node_id = input_node.node_id71on_play_pin = ":".join(input_node.outputs[0].split(":")[:-1]) # "UE.Source.OnPlay"72```7374### 4 — Add a node7576```python77# add_node(asset_path, namespace, name, variant="", major_version=1, pos_x=0, pos_y=0)78# Use list_available_nodes() to discover the correct namespace/name/variant for any node79r2 = ms.add_node(asset_path, "UE", "Sine", "Audio", 1, -200.0, 0.0)80sine_id = r2.node_id # GUID string81```8283### 5 — Set a node input default8485```python86# Set the Sine frequency to 880 Hz87# Use get_node_pins() to confirm exact input pin names before setting88ms.set_node_input_default(asset_path, sine_id, "Frequency", "880.0", "Float")8990# Read it back (issue #460):91ms.get_node_input_default(asset_path, sine_id, "Frequency") # -> "880.000000"92# All inputs at once: name / data_type / default_value / is_connected93for v in ms.get_node_input_values(asset_path, sine_id):94 print(v.name, v.data_type, v.default_value, v.is_connected)95```9697### 6 — Connect nodes9899```python100# connect_nodes(asset_path, from_node_id, output_name, to_node_id, input_name)101# Sine output pin is "Audio"; AudioOut input pin is "UE.OutputFormat.Mono.Audio:0"102ms.connect_nodes(asset_path, sine_id, "Audio", audio_out_id, audio_in_pin)103104# Read the wiring back (issue #460) — every edge touching a node, both directions:105for c in ms.get_node_connections(asset_path, sine_id):106 print(f"{c.from_node_id}.{c.from_output} -> {c.to_node_id}.{c.to_input} [{c.data_type}]")107```108109### 7 — Save110111```python112ms.save_meta_sound(asset_path)113```114115---116117## Method Reference118119### Lifecycle120121| Method | Description |122|--------|-------------|123| `create_meta_sound(package_path, asset_name, output_format="Mono")` | Create a new MetaSound Source asset. Returns `FMetaSoundResult` with `asset_path`. |124| `delete_meta_sound(asset_path)` | Delete a MetaSound asset. |125| `get_meta_sound_info(asset_path)` | Return `FMetaSoundInfo` (node count, output format, graph I/O names). |126| `save_meta_sound(asset_path)` | Save after edits. **Always call after making graph changes.** |127128### Node Discovery129130| Method | Description |131|--------|-------------|132| `list_available_nodes(search_filter="")` | List all registered External/DSP node classes. Returns `TArray<FMetaSoundNodeClassInfo>`. Each entry has `full_class_name`, `namespace`, `name`, `variant`, `inputs`, `outputs`, `display_name`. |133134### Node Management135136| Method | Returns | Description |137|--------|---------|-------------|138| `add_node(asset_path, namespace, name, variant="", major_version=1, pos_x=0, pos_y=0)` | `FMetaSoundResult` | Add a node. `node_id` on result is the GUID string. |139| `remove_node(asset_path, node_id)` | `FMetaSoundResult` | Remove node and all its edges. |140| `list_nodes(asset_path)` | `TArray<FMetaSoundNodeInfo>` | List all nodes in the graph. |141| `get_node_pins(asset_path, node_id)` | `FMetaSoundNodeInfo` | Return pin info for a single node. |142143### Connections144145| Method | Returns | Description |146|--------|---------|-------------|147| `connect_nodes(asset_path, from_node_id, output_name, to_node_id, input_name)` | `FMetaSoundResult` | Connect an output pin to an input pin. Check `r.success`. |148| `disconnect_pin(asset_path, node_id, input_name)` | `FMetaSoundResult` | Remove the connection going into an input pin. |149150### Graph I/O151152| Method | Returns | Description |153|--------|---------|-------------|154| `add_graph_input(asset_path, input_name, data_type, default_value="")` | `FMetaSoundResult` | Add a named input exposed as a runtime parameter. Appears as an `Input.<Type>` node in the graph. |155| `add_graph_output(asset_path, output_name, data_type)` | `FMetaSoundResult` | Add a named output. |156| `remove_graph_input(asset_path, input_name)` | `FMetaSoundResult` | Remove a graph input. |157| `remove_graph_output(asset_path, output_name)` | `FMetaSoundResult` | Remove a graph output. |158159### Node Configuration160161| Method | Returns | Description |162|--------|---------|-------------|163| `set_node_input_default(asset_path, node_id, input_name, value, data_type)` | `FMetaSoundResult` | Set a literal default on a node input. `data_type`: "Float", "Int32", "Bool", "String", "WaveAsset". |164| `set_node_location(asset_path, node_id, pos_x, pos_y)` | `FMetaSoundResult` | Update editor position. |165166---167168## Common DataTypes169170| Type name | Description |171|-----------|-------------|172| `Float` | 32-bit float (frequency, gain, time) |173| `Int32` | Integer |174| `Bool` | Boolean |175| `String` | Text string |176| `Audio` | Audio signal (mono channel) |177| `Trigger` | Impulse / event signal |178| `Time` | Duration in seconds (use Float in most cases) |179| `WaveAsset` | Reference to a SoundWave asset |180181---182183## Complete Example — 880 Hz Sine Tone184185```python186import unreal187ms = unreal.MetaSoundService()188189# Create asset190r = ms.create_meta_sound("/Game/Audio", "MS_Test880Hz", "Mono")191ap = r.asset_path192193# Find the AudioOut node — there are multiple nodes titled "Output" (one per interface194# pin group). Filter for the one whose inputs contain an Audio-type pin.195nodes = ms.list_nodes(ap)196audio_out_node = next(197 n for n in nodes198 if n.node_title == "Output" and any(p.endswith(":Audio") for p in n.inputs)199)200audio_out_id = audio_out_node.node_id201# Pin strings are "VertexName:TypeName" — drop only the last :TypeName to get the vertex name202audio_in_pin = ":".join(audio_out_node.inputs[0].split(":")[:-1]) # "UE.OutputFormat.Mono.Audio:0"203204# Add Sine oscillator (use list_available_nodes("Sine") to discover namespace/name/variant)205r2 = ms.add_node(ap, "UE", "Sine", "Audio", 1, -300.0, 0.0)206sine_id = r2.node_id207208# Set frequency to 880 Hz (use get_node_pins() to confirm exact pin names)209ms.set_node_input_default(ap, sine_id, "Frequency", "880.0", "Float")210211# Connect Sine "Audio" output to the AudioOut sink pin212ms.connect_nodes(ap, sine_id, "Audio", audio_out_id, audio_in_pin)213214# Save215ms.save_meta_sound(ap)216print("Done:", ap)217```218219---220221## Complete Example — One-Shot SoundWave (Gunshot)222223```python224import unreal225ms = unreal.MetaSoundService()226227# Create a Mono MetaSound Source228r = ms.create_meta_sound("/Game/Audio", "MS_Gunshot", "Mono")229ap = r.asset_path230231# Find interface nodes232nodes = ms.list_nodes(ap)233234# Input node — has "On Play" Trigger output235input_node = next(n for n in nodes if n.node_title == "Input")236input_node_id = input_node.node_id237238# Audio Output node — filter for the one with an Audio-type input239audio_out_node = next(240 n for n in nodes241 if n.node_title == "Output" and any(p.endswith(":Audio") for p in n.inputs)242)243audio_out_id = audio_out_node.node_id244audio_in_pin = ":".join(audio_out_node.inputs[0].split(":")[:-1])245246# Add Wave Player (Mono) node247# Pin names (no need to call get_node_pins — see Known Node Pins below):248# Inputs: "Play" (Trigger), "Stop" (Trigger), "Wave Asset" (WaveAsset), "Loop" (Bool)249# Outputs: "Out Mono" (Audio), "On Play" (Trigger), "On Finished" (Trigger)250wp = ms.add_node(ap, "UE", "Wave Player", "Mono", 1, -300.0, 0.0)251wp_id = wp.node_id252253# Set the wave asset254ms.set_node_input_default(ap, wp_id, "Wave Asset", "/Game/Audio/SW_Gunshot_01", "WaveAsset")255256# Wire On Play (Input) → Play (WavePlayer)257# CRITICAL: use the vertex name "UE.Source.OnPlay" — NOT the display name "On Play"258r = ms.connect_nodes(ap, input_node_id, "UE.Source.OnPlay", wp_id, "Play")259if not r.success: raise RuntimeError(f"connect On Play→Play failed: {r.message}")260261# Wire Out Mono (WavePlayer) → audio sink (Output)262r = ms.connect_nodes(ap, wp_id, "Out Mono", audio_out_id, audio_in_pin)263if not r.success: raise RuntimeError(f"connect Out Mono→Output failed: {r.message}")264265# Save266ms.save_meta_sound(ap)267print("Done:", ap)268```269270---271272## Return Type Attribute Reference273274Use these exact attribute names — guessing leads to `AttributeError`.275276### `FMetaSoundResult` — returned by most mutating methods277278| Attribute | Type | Example |279|-----------|------|---------|280| `success` | bool | `True` |281| `message` | str | `"Connected A.Audio -> B.UE.OutputFormat.Mono.Audio:0"` |282| `asset_path` | str | `"/Game/Audio/MS_Test"` |283| `node_id` | str (GUID) | `"A1B2C3D4-..."` (populated by `add_node` only) |284285```python286r = ms.connect_nodes(ap, from_id, "Audio", to_id, pin)287if not r.success:288 raise RuntimeError(r.message)289# NOT r.b_success — that attribute does not exist290```291292---293294### `FMetaSoundInfo` — returned by `get_meta_sound_info()`295296| Attribute | Type | Example |297|-----------|------|---------|298| `asset_path` | str | `"/Game/Audio/MS_Test_Blip"` |299| `asset_name` | str | `"MS_Test_Blip"` |300| `output_format` | str | `"Mono"` |301| `node_count` | int | `4` |302| `graph_inputs` | list[str] | `[]` |303| `graph_outputs` | list[str] | `[]` |304305```python306info = svc.get_meta_sound_info(asset_path)307print(info.node_count, info.output_format)308# NOT info.success, info.b_success, info.message309```310311---312313### `FMetaSoundNodeInfo` — returned by `list_nodes()` and `get_node_pins()`314315| Attribute | Type | Example |316|-----------|------|---------|317| `node_id` | str (GUID) | `"A1B2C3D4-..."` |318| `node_title` | str | `"Wave Player"` |319| `class_name` | str | `"UE.Wave Player.Mono"` |320| `inputs` | list[str] | `["Play:Trigger", "Wave Asset:WaveAsset"]` |321| `outputs` | list[str] | `["Out Mono:Audio", "On Finished:Trigger"]` |322323```python324nodes = ms.list_nodes(asset_path)325for n in nodes:326 print(n.node_id, n.node_title, n.class_name)327 # NOT n.node_class, n.node_class_name, n.name328```329330### `FMetaSoundNodeClassInfo` — returned by `list_available_nodes()`331332| Attribute | Type | Example |333|-----------|------|---------|334| `full_class_name` | str | `"UE.Wave Player.Mono"` |335| `namespace` | str | `"UE"` |336| `name` | str | `"Wave Player"` |337| `variant` | str | `"Mono"` |338| `display_name` | str | `"Wave Player (Mono)"` |339| `inputs` | list[str] | `["Play:Trigger", ...]` |340| `outputs` | list[str] | `["Out Mono:Audio", ...]` |341342```python343nodes = ms.list_available_nodes("Wave Player")344for n in nodes:345 print(n.name, n.variant, n.full_class_name)346 # NOT n.node_name, n.node_class, n.class_name347```348349---350351## Known Node Pins352353Use these instead of calling `get_node_pins` on freshly-added nodes (can time out).354355### Wave Player (UE.Wave Player.Mono)356357| Direction | Pin | Type |358|-----------|-----|------|359| Input | `Play` | Trigger |360| Input | `Stop` | Trigger |361| Input | `Wave Asset` | WaveAsset |362| Input | `Loop` | Bool |363| Input | `Pitch Shift` | Float |364| Input | `Start Time` | Time |365| Input | `Loop Start` | Time |366| Input | `Loop Duration` | Time |367| Input | `Maintain Audio Sync` | Bool |368| Output | `Out Mono` | Audio |369| Output | `On Play` | Trigger |370| Output | `On Finished` | Trigger |371| Output | `On Nearly Finished` | Trigger |372| Output | `On Looped` | Trigger |373| Output | `On Cue Point` | Trigger |374| Output | `Cue Point ID` | Int32 |375| Output | `Cue Point Label` | String |376| Output | `Loop Percent` | Float |377| Output | `Playback Location` | Float |378| Output | `Playback Time` | Time |379380### Sine (UE.Sine.Audio)381382| Direction | Pin | Type |383|-----------|-----|------|384| Input | `Frequency` | Float |385| Input | `Modulation` | Audio |386| Input | `Enabled` | Bool |387| Input | `Bi Polar` | Bool |388| Input | `Sync` | Trigger |389| Input | `Phase Offset` | Float |390| Input | `Glide` | Float |391| Input | `Type` | Enum:SineGenerationType |392| Output | `Audio` | Audio |393394### Standard Interface Nodes395396**CRITICAL:** Interface node pins use namespaced vertex names. The display name shown397in the editor is NOT the vertex name. Always use the vertex names below in `connect_nodes`.398399| Node title | Vertex name (EXACT string for connect_nodes) | Type | Direction |400|-----------|---------------------------------------------|------|-----------|401| `Input` | `UE.Source.OnPlay` | Trigger | Output |402| `Output` (Trigger) | `UE.Source.OneShot.OnFinished` | Trigger | Input |403| `Output` (Audio, **Mono**) | `UE.OutputFormat.Mono.Audio:0` | Audio | Input |404| `Output` (Audio, **Stereo** Left) | `UE.OutputFormat.Stereo.Audio:0` | Audio | Input |405| `Output` (Audio, **Stereo** Right) | `UE.OutputFormat.Stereo.Audio:1` | Audio | Input |406| `Output` (Audio, **Quad** 0–3) | `UE.OutputFormat.Quad.Audio:0` … `:3` | Audio | Input |407408The audio output format depends on the `output_format` passed to `create_meta_sound`409(`"Mono"` / `"Stereo"` / `"Quad"`). A Stereo source has **two** `Output.Audio` nodes410(Left `:0`, Right `:1`); a Quad source has four. Mono has one.411412---413414## Connecting to a Stereo (or Quad) output415416A Mono asset has a single audio sink, so `next(...)` is fine. **Stereo/Quad assets have one417audio output node per channel** — grab them all and feed each one, or channels will be silent.418419```python420# Collect every audio sink node (works for Mono, Stereo, and Quad)421nodes = ms.list_nodes(ap)422audio_out_nodes = [423 n for n in nodes424 if n.node_title == "Output" and any(p.endswith(":Audio") for p in n.inputs)425]426# Sort by channel index so [0]=Left, [1]=Right (the vertex name ends with ":<index>:Audio")427audio_out_nodes.sort(key=lambda n: n.inputs[0])428429# Mono: one node. Stereo: two (Left, Right). Quad: four.430# Example — send a mono signal (e.g. a Mixer "Out") to BOTH stereo channels:431for out_node in audio_out_nodes:432 sink_pin = out_node.inputs[0] # e.g. "UE.OutputFormat.Stereo.Audio:0:Audio"433 r = ms.connect_nodes(ap, source_id, "Out", out_node.node_id, sink_pin)434 if not r.success: raise RuntimeError(r.message)435```436437For a true stereo image, drive Left and Right from different signals (e.g. two oscillators,438or a stereo `Audio Mixer (Stereo, N)` whose `Out L` / `Out R` feed the two sinks). To get a439stereo mixer, search `list_available_nodes("Mixer")` for a `(Stereo, …)` variant — the440`(Mono, N)` mixer has a single `Out` pin and only fills one channel on its own.441442> `connect_nodes` accepts the full `"VertexName:TypeName"` pin string (it strips the trailing443> `:TypeName`), so you can pass `n.inputs[0]` directly without manual splitting.444445---446447## Notes448449- **Save after every batch of edits**, not after each individual operation, to avoid excessive disk I/O.450- `list_available_nodes("")` returns **all** registered node classes (~400+). Use a filter like `"Sine"`, `"Delay"`, `"Gain"` to narrow the results.451- Node pin names for standard nodes use UE display names (e.g. `"In Frequency"`, `"Out"`, `"Audio:0"`). Use `get_node_pins()` to confirm exact names.452- A MetaSound Source has **multiple nodes with `node_title == "Output"`** — one per interface pin group (e.g. `"On Finished"` Trigger, `"Out Mono"` Audio). To find the audio sink node, filter for the Output node whose inputs contain an Audio-type pin: `next(n for n in nodes if n.node_title == "Output" and any(p.endswith(":Audio") for p in n.inputs))`. Pin strings from `list_nodes` are `"VertexName:TypeName"` — you can pass them directly to `connect_nodes`, which now automatically strips the trailing `:TypeName` suffix. Manual stripping (`":".join(pin.split(":")[:-1])`) still works but is no longer required.453- Node namespace/name/variant values differ from what the MetaSound editor displays. Always call `list_available_nodes("keyword")` to discover the correct values; do not guess.454- MetaSound Sources do **not** support SoundCue-style `SoundNodeWavePlayer` — use the `WavePlayer` MetaSound node instead (discover via `list_available_nodes("Wave Player")`).455- **Graph inputs appear as `Input` nodes** in the graph (`class_name == "Input.Float"`, `"Input.Bool"`, etc.). When filtering for the standard interface Input node (the one that carries `On Play`), always match by `class_name == "Input.Trigger"` — not by `node_title == "Input"` alone, which will also match graph input nodes.456- **AudioMixer pins interleave audio and gain:** `Audio Mixer (Mono, 2)` has pins `["In 0:Audio", "Gain 0:Float", "In 1:Audio", "Gain 1:Float"]`. Never index by position — always filter for `:Audio` typed pins when connecting audio signals: `audio_ins = [p.split(":")[0] for p in mixer_node.inputs if p.endswith(":Audio")]`.457- **Stereo MetaSound Sources have TWO audio output sinks, one per channel.** A Stereo source's audio output pins are `UE.OutputFormat.Stereo.Audio:0` (Left) and `UE.OutputFormat.Stereo.Audio:1` (Right) — exposed as **two separate `Output.Audio` nodes**, not one. (Quad has four: `:0`–`:3`.) You must feed **both** channels or one side will be silent. The single-`next()` audio-sink finder used for Mono only grabs one channel — for Stereo, collect every audio output node. See **Connecting to a Stereo/Quad output** below.458459## Sample scripts (run via `execute_python_code`)460461- **`scripts/create_metasound.txt`** — create a MetaSound Source and list available node types.