Skip to content

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.h reserves 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] BaseDefaultSubmix points at SMX_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. DefaultSoundClassName is SCL_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 of SCL_SFXCombat holding 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.4 UEternalMusicSubsystem is 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 to SMX_Ambience output volume modulation); the mapping lives in the subsystem. SCB_Greed stays 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_CombatEffectUCombatEffectsManager::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] PhysicalSurfaceToTagCombat.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.Audit enforces full coverage.
  • Footstep foley — ALS footstep notifies key directly off EPhysicalSurface in AlsFootstepEffectsSettings (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-review applies to any code that touches this rule.

Enforcement

Eternal.Audio.Audit (commandlet, Source/ProjectEternalEditor/.../Audio/AudioAuditCommandlet.cpp):

UnrealEditor-Cmd.exe <project.uproject> -run=AudioAudit -unattended -nopause -nullrhi [-strict]

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).