Audio Architecture¶
How sound is routed, prioritized, and triggered in Project Eternal. Built on the UE 5.8 native stack
(MetaSounds + Quartz + Audio Modulation + Submixes + AudioGameplayVolume) — no middleware (ruling D1,
ImplementationDocs/AudioAndMusic.plan.md; FMOD via AudioLink is the written fallback, not the plan).
The mix hierarchy (the law)¶
Priority order the whole graph encodes:
player abilities > incoming threat tells (boss/elite wind-ups) > kill & loot confirms > enemy foley/vocals > ambience/music bed
Two protected rules:
- Heartbeat is stamina-owned. It lives on its own bus that survives voice-limiting and ducks
everything else. Nothing else may play on it (
ScreenEffectsConfig.hreserves the channel). - Loot/item audio is state-keyed, never rarity-keyed (D3). There are no rarity tiers in this game; stingers key off item state (Sealed / Awakening / Greed bands / Consumption) and affix significance.
The bus graph¶
All routing assets live in Content/Audio/_Common/Mix/ and are registered in
Config/DefaultGame.ini under [/Script/ProjectEternal.EternalAudioSettings]
(class: UEternalAudioSettings, Source/ProjectEternal/Public/Audio/Data/EternalAudioSettings.h).
SMX_Master ── SMX_Music SCL_Master ── SCL_Music
├─ SMX_SFXCombat ├─ SCL_SFXCombat
├─ SMX_SFXWorld ├─ SCL_SFXWorld
├─ SMX_Ambience ├─ SCL_Ambience
├─ SMX_UI ├─ SCL_UI
├─ SMX_Voice ├─ SCL_Voice
└─ SMX_Heartbeat └─ SCL_Heartbeat
- Submixes (
SMX_) carry the signal and are where effects/analysis/ducking attach.[/Script/Engine.AudioSettings] BaseDefaultSubmixpoints atSMX_Master, so any sound that does not name a submix still lands inside the graph. - SoundClasses (
SCL_) exist for legacy cue/wave grouping and coarse volume control.DefaultSoundClassNameisSCL_Master. The class tree mirrors the submix tree one-to-one. - Concurrency (
SCC_) caps voices per bus. Combat one-shots: 16 voices, StopLowestPriority (priority stealing). Heartbeat: 1 voice. Music: 2 (enough for a crossfade). - Break class (
SCL_SFXCombatBreak) is a child ofSCL_SFXCombatholding the sounds that mark a stance break. It exists so those sounds can duck their own parent — see Ducking a beat out of its own bus. - Control buses (
SCB_CombatIntensity,SCB_Depth,SCB_Greed) are the AudioModulation hooks for gameplay-driven mixing. Since plan 2.4UEternalMusicSubsystemis the sole writer of CombatIntensity (music state → 0..1) and Depth (node depth, plus a sub-percent shared stem detune — the cheap first cut of depth-distortion). The combat duck rides a fourth bus,SCB_AmbienceVolume(volume semantics, attached toSMX_Ambienceoutput volume modulation); the mapping lives in the subsystem.SCB_Greedstays unbound until plan 4.1.
The legacy foley submixes (SMX_Final/SMX_Foley/SMX_Ambient/SMX_Reverb under
Content/Audio/Movement/Mix/) came with the (since-removed) foley marketplace pack; the surviving
MSS_FoleySound_* sources are used only by the Prelude cutscene today. SMX_Final (their root) is
parented under SMX_SFXWorld, so the whole graph is one tree. Merging them away entirely is Phase 1
work.
Trigger layers (how a sound gets played)¶
| Layer | Mechanism | Status |
|---|---|---|
| Montage-timed combat SFX | UAnimNotify_CombatEffect → UCombatEffectsManager::TriggerSoundEffect → the row's FAudioEffectLayer |
Working, data-driven, mostly unpopulated |
| Weapon × surface impacts | UCombatEffectsConfiguration impact matrix, surface tag from UCombatEffectsSettings::GetSurfaceTag |
Working; surface axis fully mapped (see below) |
| GameplayCues (GAS) | UEternalGameplayCueManager + UGameplayCueAudioConfig registry (tag→sound rows, zero code); weapon-identity/aura loops on their configs |
Working since plan 1.2; loop content is an external-delivery gap (plan 3.x) |
| Item pickup/equip | FItemManifest fragments (2D sounds) |
Working; all equipment manifests covered |
| UI buttons | FSlateSound on each plain UButton's style (fleet-wired; CommonButton migration later moves these to style assets) |
Working since plan 1.4 |
| Music | UEternalMusicSubsystem (GameInstance, client-only) + UMusicStateAsset stems sample-locked on a Quartz clock; bar-quantized state fades. Zone sets the base state; the replicated State.InCombat tag (UCombatEngagementSubsystem, server) overlays Combat |
Working since plan 2.1; placeholder content (menu theme) until commissioned stems land |
| Ambience beds | AudioGameplayVolumes + bed assets | Does not exist yet; plan 2.3 |
| Screen effects | FScreenEffectDefinition.Sound — big-hit one-shot + low-health-gate sting, owner-local 2D |
Read since plan 1.4; assets are an external-delivery gap. Never a heartbeat, never a loop |
Ducking a beat out of its own bus¶
A poise break happens inside the hit that caused it, so its shatter sound arrives on the same bus, in the same frame, as a full impact row. The first attempt at fixing that turned the shatter down relative to the hit — the wrong direction, and it made the problem worse.
The shipped answer is a sidechain at mix level:
SCL_SFXCombat <-- ducked to ~0.45 for ~0.3s
| by a passive mix, children excluded
+-- SCL_SFXCombatBreak <-- the shatter lives here, so it is NOT ducked
Mix_SunderDuck is a passive USoundMix that lowers SCL_SFXCombat while excluding its children, with a fast fade
in and a slower fade out. The break sound sits on the child class and carries a raised priority, so it wins voice
stealing as well. Zero gameplay code — the whole fix is asset configuration.
Generalised: when one sound must own a beat, give it its own child class and duck the parent. Turning the competing sounds down individually does not scale and inverts the moment the row is re-authored.
The wider rule this serves — one dominant channel per beat, big events replace small ones rather than adding to them — is documented in Break Presentation, which covers the non-audio channels (VFX, camera shake, hit stop, screen flash) the same beat also claims.
Surface taxonomy¶
The 10 physical surfaces in DefaultEngine.ini (+PhysicalSurfaces) are the domain. Two
consumers exist:
- Combat impacts —
[/Script/ProjectEternal.CombatEffectsSettings] PhysicalSurfaceToTag→Combat.Surface.*(semantic set: Flesh, Metal, Stone, Wood, Dirt, Water, Sand, Default). Impacts don't need gravel-vs-rocks resolution, so several physical surfaces map to one combat tag (Gravel/Rocks→Stone, Grass/Dirt/Mud→Dirt).Eternal.Audio.Auditenforces full coverage. - Footstep foley — ALS footstep notifies key directly off
EPhysicalSurfaceinAlsFootstepEffectsSettings(no tag map). Coverage there is per-surface content, not config — authored in Phase 1.
Replication discipline¶
Audio is client-side cosmetic — a dedicated server plays nothing, ever, and that is by design.
- Montage notifies ride montage replication (no RPCs).
- Ability/status audio rides GameplayCues (the sanctioned multiplayer path).
- UI/screen audio is owner-local.
- No bespoke audio RPCs.
/mp-reviewapplies to any code that touches this rule.
Enforcement¶
Eternal.Audio.Audit (commandlet, Source/ProjectEternalEditor/.../Audio/AudioAuditCommandlet.cpp):
Reports combat-surface coverage, D7 naming compliance, SoundClass routing, mix-graph registration gaps,
combat-effect rows with no audio layer (opt-out: bExpectedSilent for intentionally-silent rows), and
montage windup coverage (a ComboHitWindow montage must carry an audio notify near the first window).
Report file: Saved/AudioAudit/AudioAudit_<ts>.md. Non-strict always exits 0; -strict fails on
structural errors. Core logic is UObject-free in AudioAuditCore with a spec
(ProjectEternal.Editor.AudioAudit). The ability validator additionally warns on projectile abilities
with no travel audio (the windup/travel/impact layer model, 02_Audio_Authoring.md).