Audio Authoring¶
How to name, place, and wire a sound. Architecture and routing: 01_Audio_Architecture.md.
Naming standard (D7)¶
Engine assets — prefix is determined by the asset's class, enforced by Eternal.Audio.Audit:
| Class | Prefix | Example |
|---|---|---|
| SoundWave | SW_ |
SW_Sword_Blood_01 |
| MetaSound Source | MSS_ |
MSS_FoleySound_Dirt_Run |
| MetaSound Patch | MSP_ |
MSP_StemMixer |
| SoundCue (legacy only — new work uses MetaSounds) | SC_ |
SC_Equip_Jewelry |
| Sound Attenuation | SAT_ |
SAT_FootstepsSoundAttenuation |
| Sound Submix | SMX_ |
SMX_SFXCombat |
| SoundClass | SCL_ |
SCL_Heartbeat |
| Sound Concurrency | SCC_ |
SCC_SFXCombat |
| Control Bus (AudioModulation) | SCB_ |
SCB_Greed |
| Modulation Patch (AudioModulation) | SCP_ |
SCP_AmbienceDuck |
| Reverb Effect | RVB_ |
RVB_Cave_Reverb |
| Submix Effect Preset | SFXP_ |
SFXP_Reverb_Default |
Descriptors are PascalCase_With_Underscores, variations zero-padded two digits (_01).
System context goes in the name when the folder alone is ambiguous (SW_Footstep_Carpet_01,
SW_Foley_Dirt_01).
External source files (WAVs outside the engine) use a different convention —
see 03_External_Deliverable_Spec.md. Raw masters live outside Content/ (sibling of ArtSource/).
Folder layout¶
Content/Audio/_Common/Mix/— the entire routing graph (submixes, classes, concurrency, control buses).Content/Audio/<System>/— playable assets grouped by system (Combat, Footsteps, Movement, Music, UI, Levels/, Creature, …). - Level-specific one-offs live under
Content/Audio/Levels/<Level>/.
Wiring recipes (what exists today)¶
Montage-timed combat sound (swing, whoosh, effort)¶
Two routes, by what the sound needs:
- Plain one-shot (swing whoosh, creature vocal): add an
AnimNotify_PlaySoundon the montage'sAudionotify track, ~0.05–0.15s before the hit window opens. This is the established convention — attack montages across all weapon sets playMSS_Weapon_Swingthis way, and creature attack/hit montages play theSC_Creature_Archanoid_*cues. Enforced: the AudioAudit "Montage windup audio" section warns on any montage that opens aComboHitWindowwith no audio notify by 0.25s after the first window opening (the grace absorbs hand-placed sounds that ride the swing feel). - Config-driven effect (audio + VFX + shake + hitstop as one authored row): add a
UAnimNotify_CombatEffectnotify with an effect tag, then add/edit that tag's row inContent/DataAssets/Combat/CombatEffectsConfigurationEffectConfigurationsand add an Audio layer (FAudioEffectLayer) to itsLayerswith the sound. A row plays only the channels it has layers for — an audio-only row is just a single Audio layer, no hit-stop zeroing needed. A row that should stay silent (VFX/shake-only) must setbExpectedSilent— unflagged silent rows warn in the audit, and a flagged row that still carries an audio layer errors.
Either way the notify rides montage replication — remote clients hear it with zero extra work.
Weapon × surface impact¶
Rows live in CombatEffectsConfiguration.ImpactMappings as (Combat.Weapon.* × Combat.Surface.*)
→ FCombatEffectConfig. Fallback order: exact pair → WeaponTypeDefaults → SurfaceTypeDefaults
→ DefaultHitEffect. The surface tag arrives automatically from the hit's physical material
(UCombatEffectsSettings::GetSurfaceTag). All 10 surfaces resolve; you only author the pairs that
should sound distinct.
Charge audio¶
ChargeComponent fires Audio.Charge.Weapon.Level1-3 into the effects manager; the tags are mapped
in EffectConfigurations to SC_Charge_Weapon_Level1-3 (pitched 0.9/1.0/1.15 duplicates of the
AttackUp riser — placeholder until commissioned charge SFX land). Swapping the sound = editing the row.
Item pickup / equip / inventory drop¶
FItemManifest fragments carry the item's inventory-interaction 2D sound (played on pickup and on
dropping the item into the grid) — author on the item's manifest data asset:
- Equipment items:
FEquipmentFragment.ItemSound(theSC_Equip_*family covers metal/leather/jewelry/glass; all equipment manifests are wired). - Non-equipment items:
FItemSoundFragment.ItemSound—UInventoryStatics::GetItemSoundfalls back to it when no equipment fragment exists. Wired: stone plates →SW_StonePlate_Placement, glyphs →SW_GlyphSocket(their glyph-wall placement sounds are separate, onStonePlateFragment.PlacementSound/ the glyph controller). Cores, fragments and quest items stay silent until fitting commissioned SFX exist (organic/bone thunks, plan 3.x); the state-keyed loot drop stingers are plan 4.2.
UI button click / hover¶
Plain UButtons carry the sounds directly on their style: PressedSlateSound →
SW_UI_Button_Click, HoveredSlateSound → SW_UI_Button_Hover (both routed SCL_UI). The whole
widget fleet was wired 2026-07-07; new buttons should set both fields (headless recipe: load the
button template by subobject path <WBP>.<Name>:WidgetTree.<Button> and round-trip widget_style).
Full-screen click-catchers (e.g. the sequence skip button) stay silent on purpose. When the planned
CommonButton migration happens, these move to shared UCommonButtonStyle assets — one place instead
of per-widget fields.
Screen-effect sounds¶
FScreenEffectDefinition.Sound is read by UScreenEffectsController (owner-local 2D): the big-hit
pulse plays its sound with the visual pulse; the low-health vignette plays its sound once when
the Below35 gate engages. Neither may be a heartbeat or a loop — stamina owns the heartbeat channel.
No assets are authored yet (external-delivery slot, plan 3.x).
Ability / status audio via the GameplayCue registry (zero code)¶
Any GameplayCue tag can play audio by adding a row to
Content/DataAssets/Audio/GameplayCueAudioConfig (registered under Project Settings → Game →
GameplayCue Audio). UEternalGameplayCueManager consults the registry after normal cue routing:
Sound— one-shot at the cue location on Executed and OnActive (burst cues, apply stings).LoopingSound— attached to the target from OnActive/WhileActive until Removed (status/duration cues; late joiners pick the loop up via WhileActive without re-hearing the apply).VolumeMultiplier/PitchMultiplier— placeholder tuning without duplicating the asset.
Lookup is by exact tag. Do not add rows for cues whose audio already flows through a dedicated
config (weapon×surface impacts via GameplayCue.Combat.Impact, parry, weapon-identity loops, aura
loops) — they would double-play. Wired today: GameplayCue.Status.Shock → SC_SFX_Loop_Electric
(shocked characters crackle for the status duration).
A cue that also owns a CombatEffectsConfiguration row must pick one path for sound. The poise cues are the
worked example: GameplayCue.Poise.Sunder and .Stagger take their sound from the registry, while their
Effects.Poise.* rows carry bExpectedSilent (no audio layer) and own only VFX / shake / hit stop. GameplayCue.Poise.ParryReflect went
the other way and had its registry row removed entirely — which is why the debug window below will correctly
report it as "no registry row".
Note where its sound actually ended up, because it is not the obvious row: Effects.Poise.ParryCrack (the cue's
own row) is VFX-only. The fold described below moved the sound onto the defender's Effects.Parry row
(MSS_Combat_Parry), so the attacker-side reflect cue and the sound that plays for it live on different rows. If
you are tracing "where does the parry sound come from", neither the registry nor the cue's own row is the answer.
Layering a beat against the hit it lands inside¶
Combat state cues (a stagger, a break, a parry crack) fire on the same frame as the impact row of the hit that caused them. Three rules, learned the hard way:
- Do not solve masking with volume. Ducking the state cue under the hit is backwards, and turning it up just makes both loud. Give the beat its own SoundClass and duck the parent — see Ducking a beat out of its own bus.
- Subordinate cues should stay subordinate. The stagger "not yet" tell is deliberately quieter than the break shatter; contrast is what makes the break read, and it is spent by anything that raises the floor.
- Fold competing sounds rather than layering them. When the parry clang and the posture-crack fought, the crack
replaced the clang in the
Effects.Parryrow — one sound, one meaning.
Round-robin presets keep repeated beats from reading as a loop: the poise stagger/sunder sets are presets of
MSS_Weapon_Punch with round-robin selection and mild pitch variation, which is the cheapest way to add variation
without new MetaSound graphs.
Name the asset after what it is, not where it came from. MSS_Poise_ParryCrack was renamed to
MSS_Combat_Parry once the fold above made it the only parry sound rather than the poise-tier layer — the
poise-namespaced name implied a layering that no longer existed. The Effects.Poise.ParryCrack tag kept its
name, because it now unambiguously means the attacker-side reflect row, which is correctly poise-scoped.
Weapon-identity element loop¶
FWeaponEffectConfig (rows in Content/DataAssets/Combat/WeaponEffectsConfiguration) carries
LoopingSound + LoopAttenuation next to its Niagara/overlay fields — attached at the weapon
socket while the identity cue is active. No element loop assets are owned yet (the only live
identity row is Bleed); commissioned SFX (plan 3.x) fill these.
Aura loop¶
UAuraVisualConfig layer 4 (bUseAudioLoop + LoopSound + LoopAttenuation) — attached to the
aura owner, faded out when the aura GE is removed. No aura loop assets owned yet.
Routing a new sound into the mix¶
Set the wave/MetaSound's Sound Class to the bus it belongs to (SCL_SFXCombat, SCL_UI, …) and
its attenuation to a shared SAT_ asset. MetaSound sources set their output submix likewise. The
audit flags sounds with no class and sounds whose class chain doesn't root in SCL_Master.
(All legacy waves were routed 2026-07-07 — the audit baseline is 0 routing violations.)
Ability audio layers (windup / travel / impact)¶
Every ability owes its applicable layers (plan 1.3 model; the create wizard lists them per archetype):
- Windup — the action-initiation sound on the montage (swing whoosh, charge loop, body effort). Enforced by the AudioAudit montage check above.
- Travel — only for abilities that spawn something moving:
ProjectileConfig'sLaunchSound/FlightLoopSound/FlybySound(both the player and enemy projectile hierarchies). The ability validator warns while all three are unset. - Impact — the connect, via the weapon×surface impact matrix or the ability's impact cue. The matrix always resolves (DefaultHitEffect thud fallback); distinct pairs are external content (plan 3.x).
A plain melee swing is two layers (windup + impact); travel only exists when something flies.
Music (stateful stem bed, plan 2.1/2.2)¶
UEternalMusicSubsystem (GameInstance, client-only) plays exactly one UMusicStateAsset on a
Quartz clock. Author music as an asset, never as code:
UMusicStateAsset(Content/DataAssets/Audio/MusicState_*): tempo/meter + named stems + a state→gain matrix (Explore / Combat / Boss / Rest / RemnantRealm). All stems start sample-locked and never stop; a state change is a bar-quantized gain fade. Optional per-stateEnterStinger+ aLiberationStinger(domain boss kill).- Stem sounds must: route to
SCL_Music, loop cleanly, and setVirtualizationMode = PlayWhenSilent— a muted stem that loses its transport position desyncs the whole bed when it fades back in. The audit enforces all three plus row/stem cross-references. - The active asset is
EternalAudioSettings.DefaultMusicState(DefaultGame.ini) until per-Domain assets land (external deliverables, plan 3.x). Today's content is the recovered menu theme as a single placeholder stem. - Base state comes from where the player is: world-map node type (
IsSafeZone→ Rest,DomainHQ→ Boss until the dungeon clears) and the Remnant Realm subsystem. A dungeon or HQ clear falls back to the zone's base state structurally, not on a timer. - Combat overlays the base state.
UCombatEngagementSubsystem(server) maintains the replicated loose tagState.InCombaton a character's ASC for exactly as long as at least one living enemy has it as an attack target. The director reads the tag off the local pawn and holds Combat forCombatHoldSecondspast the last enemy disengaging, so a lull mid-fight doesn't cut the bed. Combat cannot outrank Boss or Remnant Realm — priority lives inEternalMusic::ResolveState.
Control buses (plan 2.4)¶
The music director is the sole writer: SCB_CombatIntensity follows the music state (0 rest →
1 boss), SCB_Depth follows the current node's effective depth (DepthNormalizationMax saturates
it), and SCB_AmbienceVolume (volume semantics, default 1.0, attached to SMX_Ambience's output
volume modulation) carries the combat duck — the intensity→duck mapping lives in the subsystem,
not in an opaque curve asset. Never write these buses from gameplay code; route new inputs through
the subsystem so the mix stays one system. (When mappings multiply, migrate to AudioModulation
patches — SCP_ prefix is reserved.)
Not built yet (don't look for it)¶
- Ambience beds / AudioGameplayVolumes — plan 2.3.
UUISoundSet— deliberately not built: plain-button style fields are the seam until the CommonButton migration (Oliwer ruling 2026-07-07), at which point sharedUCommonButtonStyleassets take over.
Verifying your work¶
Zero naming violations is the standing baseline (established 2026-07-07). If your change adds one, rename before committing. Spam rule for later phases: any ability castable faster than 1/s needs round-robin variations + a concurrency cap.
The Eternal.Audio debug window (in PIE)¶
The audit is static analysis; it cannot tell you what you actually heard. The Cog window Eternal.Audio answers
that at runtime:
| Pane | Shows |
|---|---|
| Live sounds | Active AudioComponents — what is playing right now, and what is looping when it shouldn't be |
| Routed-cue history | The last 128 gameplay cues that reached the registry, including the ones that matched no row |
It exists because of a specific failure that no other tool catches: a new cue sound was authored, the beat played, and the honest report was "I mostly hear the hit thud" — with no way to tell whether the new sound was masked, mis-routed, or never played at all. "No row" entries are recorded rather than dropped for exactly that reason; a cue that legitimately owns its sound elsewhere will show up as "no row" and that is expected (see the poise cues above).
Recording lives in UEternalGameplayCueManager, so the history covers every routed cue regardless of who fired it.