Audio System¶
How SC6 plays sounds. The short version: the audio middleware is
CRI ADX2 / ATOMCRAFT (not Wwise, not FMOD), the audio thread runs
independently of Actor::Tick, and MoveVM bytecode drives every
in-battle sound through either a per-character event-cue scheduler or a
per-move audio-palette tween. Voice-cue tables, weapon-SE bank routing,
and DramaticVoice persona triggers are all UDataAsset-driven.
All addresses absolute (image base 0x140000000).
Middleware identification¶
| Evidence | Confirms |
|---|---|
.acb file paths in strings (e.g. Sound/_resource/SE/Weapon/CS_NNN_M.acb, Sound/_resource/Voice/JP/Character_Voice/J_VO_CV_NNN_OPTION.acb) |
CRI Atom Cue Binary format |
String vocabulary: VoiceCueID, VoiceCueSheet, CueSheet, VoiceCueIDConvTable |
CRI ADX2 / ATOMCRAFT API surface |
| 0x150-byte singleton manager owning sheet table + cue-id → instance-id map | CRI Atom Manager pattern |
criAtomEx-style player API (cue id + sheet key → voice instance handle) |
ADX2 player object |
No Wwise: no
AK*symbols, no.bnkfiles. No FMOD: noFMOD_*strings. CRI ships an Unreal plugin (UAtomComponent— not the stock UEUAudioComponent) for the in-world emitter binding. UE owns the spatialization transform; CRI owns the mixer.
High-level architecture¶
MoveVM bytecode (per-move authored, per-frame)
│
├── opcode 0x3A ──► LuxMoveVM_DispatchEffectOp @ 0x140376B20
│ └── LuxMoveVM_TriggerAudioPaletteCallback @ 0x1403693D0
│ └── writes FLuxAudioPaletteTween at chara+0x971A8..+0x971B8
│ (audio-mix parameter lerp — does NOT play any sound)
│
└── anim-notify / "secondary action stack" entries
└── Event Cue Scheduler at chara+0x95750
└── LuxMoveVM_TickCharaEventCueScheduler @ 0x14038BD60
└── LuxMoveVM_SecondaryActionStackOp(chara+0x95788, ...)
└── LuxAudio_FireSoundCue_ViaVfxDispatcher @ 0x1403110B0
└── g_pLuxVfxDispatcher.vtable[0x38] (sound submit slot)
└── LuxAudio_PlayCueIdByByte @ 0x140550900
├── LuxAudio_GetCriAtomManagerSingleton @ 0x140547D60
├── LuxAudio_ResolveCueSheetEntryByKey @ 0x140547210
└── LuxAudio_RegisterActiveVoiceInstance @ 0x14054F8B0
└── CRI ADX2 player (independent audio thread)
UE side (mute / spatialization only — CRI's Unreal plugin):
chara->vftable[0x138]() → animSubObj+0x140 audioBindings
→ walk children for first with `+0x30` set
→ UAtomComponent (vftable[0xA08] = active toggle)
source: function plates at
0x1403693D0,0x140550900,0x14054F8B0,0x140547D60,0x140547210,0x14038BD60,0x1403110B0.
Thread model — what runs where¶
| Component | Thread | Frozen by g_LuxBattle_VMFreezeByte? |
|---|---|---|
| CRI ADX2 player (sample mix, voice output) | own audio thread | no — in-flight voices keep playing |
| MoveVM event cue scheduler @ chara+0x95750 | game thread (LuxBattle_PerFrameTick) |
yes — gated by LuxMoveVM_GetTimeDilationScalar |
LuxAudio_PlayCueIdByByte cue submission |
game thread (called from scheduler) | yes (transitively) |
UAtomComponent mute / active toggle (CRI's UE plugin) |
game thread | no |
Audio_RandomTick @ 0x140399B70 |
game thread | unverified — sits outside the simulation loop |
Implication for any "pause" feature: freezing the simulation (e.g. via
VMFreezeByte) stops new cue submission but does not silence voices already in flight. To silence them, either mute per-chara viaUAtomComponent::vftable[0xA08], or duck the relevant CRI bus viaSoundBusDackingData.
Fast-forward burst-playback
If you let the simulation race forward at high time-dilation
(e.g. a "fast skip" UX), several frames' worth of cue triggers
collapse into a single tick and the CRI player gets a burst of
overlapping voices. Two mitigations: (a) gate the body of
LuxAudio_FireSoundCue_ViaVfxDispatcher @ 0x1403110B0 while
fast-forwarding, or (b) set UAtomComponent::vftable[0xA08] = false
on every chara before the burst.
Two paths from MoveVM to audio¶
Path A — Audio palette tween (opcode 0x3A)¶
Per-move-scripted scalar lerp for an audio-mix parameter (volume / duck level / palette cross-fade). Does not play sound.
// LuxMoveVM_TriggerAudioPaletteCallback @ 0x1403693D0
void LuxMoveVM_TriggerAudioPaletteCallback(
longlong pChara,
float flTargetValue, // lerp target
int nFrameCount, // 0 = snap, >0 = lerp over N dilated frames
uint dwPaletteId // palette bank id (dispatcher pre-writes 1)
);
State written to chara+0x971A8..+0x971B8 (struct FLuxAudioPaletteTween):
| Offset | Field | Type |
|---|---|---|
+0x971A8 |
dwPaletteId |
uint — palette bank id |
+0x971AC |
nFramesLeft |
int — dilated-frame countdown |
+0x971B0 |
flCurrentValue |
float — read by downstream mix |
+0x971B4 |
flTargetValue |
float |
+0x971B8 |
flDeltaPerFrame |
float — (target − current) / frames |
Path B — Sound cue submission (event-scheduler)¶
The actual sound-playback path. The MoveVM-scripted "event cue scheduler"
struct at chara+0x95750 is ticked every dilated frame and dispatches
two kinds of entries:
| Entry type | Field +0x28 |
Dispatch |
|---|---|---|
| Sound cue | < 0 |
LuxMoveVM_SecondaryActionStackOp(chara+0x95788, chara, argA, argB) |
| Anim-notify event | ≥ 0 |
LuxMoveVM_SecondaryActionStackPush(chara+0x95788, chara, type, count−1) |
The sound-cue port at chara+0x95788 ultimately reaches the CRI player
through LuxAudio_PlayCueIdByByte. The scheduler clock at
chara+0x95750+0x38 advances by LuxMoveVM_GetTimeDilationScalar(chara),
so hitstop or freeze stalls the schedule.
source:
LuxMoveVM_TickCharaEventCueScheduler @ 0x14038BD60plate.
Voice families — three distinct selection drivers¶
Three independent voice systems, picked from different sources:
1. Per-move battle voice (the bulk of in-battle audio)¶
Authored in the move bytecode itself. The cue id is part of the move script and fires from the event cue scheduler at the scripted frame. Selection is fully contextual (move + chara state at cue time).
2. Ranked-match flavor voice¶
ALuxBattleChara_SelectAndPlaySound_Ranked @ 0x1403C8950 picks a byte
cue id from one of three pools based on a time-derived hash:
| Condition | Pool global |
|---|---|
| Not ranked-eligible | DAT_144145E60 |
| Round-ready, non-ranked | DAT_144145E70 |
| Round-ready and Ranked Match | DAT_144145E80 |
The hash seed is FPaths_GameLogDir() (the digits in the log-file path),
so the pick is non-deterministic across runs, replay playbacks
included. These are pre- and post-round flavor lines only — they do not
affect gameplay.
3. DramaticVoice (story persona)¶
ULuxDramaticVoiceDataAsset (registration at 0x140161AD0). Each persona
is keyed by a DramaticVoiceID resolved per battle at battle-setup time
from the active battle-rule DataTable:
// ResolveDramaticVoiceID_FromBattleSetting @ 0x1405F2910
// 1. If PlayerLeft.UseAssistor → return -1 (no dramatic voice)
// 2. Else read PlayerLeft.DramaticVoiceID
// 3. Else read PlayerRight.DramaticVoiceID
// 4. Default -1
Once the persona is chosen, individual lines are triggered contextually
during the match by events on the ELuxBattleDramaticVoiceTriggerType enum:
enum class ELuxBattleDramaticVoiceTriggerType : uint8 {
Reply = 0,
BattleStart = 1,
BattleTime = 2,
PlayerLife = 3,
PlayerWin = 4,
ReversalEdge = 5,
ReversalEdgeStart = 6,
ReversalEdgeMove = 7,
ReversalEdgeWin = 8,
CriticalEdge = 9,
SoulCharge = 10,
ChangeMood = 11,
ChangeMotion = 12,
CriticalEdgeHit = 13,
MissionBattleStartCount = 14,
MissionPlayerWin = 15,
MissionCriticalEdge = 16,
ENUM_MAX = 17,
};
source: enum string list at
0x143379400 – 0x14337A130.
DramaticVoice IDs are recorded in the replay header, so dramatic-voice playback is deterministic across replay viewing (same persona, same trigger conditions, same cue choices).
Chara offset map — audio state on ALuxBattleChara¶
| Offset | What | Owning function |
|---|---|---|
+0x394 |
u32 audio active/mute bitfield. Bit 0 = should-play gate, Bit 1 = force-active, Bit 2 = force-mute, Bit 3 = always-active | LuxBattleChara_SyncAudioActiveState_FromBattleFlags @ 0x140438980 |
+0x95750 |
Event cue scheduler struct (timing, trigger list at +0x70, side-byte at chara+0x23C) |
LuxMoveVM_TickCharaEventCueScheduler @ 0x14038BD60 |
+0x95788 |
Secondary action stack (the sound-cue submit port) | scheduler + dispatcher case 0x3A |
+0x971A8..+0x971B8 |
FLuxAudioPaletteTween — opcode 0x3A lerp state |
LuxMoveVM_TriggerAudioPaletteCallback @ 0x1403693D0 |
+0x971E8 |
Visual palette controller (NOT audio — name collision warning) |
Bit 0 of chara+0x394 is audio-only
Flipping bit 0 of chara+0x394 mutes the chara's voice but does
not pause world simulation. A long-standing "Game Pause" CE
cheat hooks this site and gets confused by the partial effect. The
real world-tick pause is g_LuxBattle_VMFreezeRecord.bVMFreezeByte
via movement.md /
replay-system.md.
VFX + Audio dispatcher (g_pLuxVfxDispatcher)¶
Despite the "Vfx" name, g_pLuxVfxDispatcher is the unified VFX + audio
event sink — the convergence point for every per-frame side-effect MoveVM
bytecode emits. Both VFX spawns and CRI cue submissions route through its
vtable.
Relevant slots:
| Slot | Role |
|---|---|
vt[0x30] |
End-of-life cleanup A (called when a cue's owning effect retires) |
vt[0x38] |
Sound cue submit — bridge from MoveVM cue scheduler to LuxAudio_PlayCueIdByByte |
vt[0x40] |
End-of-life cleanup B |
vt[0x70] |
Audio palette change — applies the FLuxAudioPaletteTween value to the mixer |
LuxAudio_GetCriAtomManagerSingleton @ 0x140547D60 has 201 callers;
nearly every audio path passes through it. If you're hooking the audio
layer at a single chokepoint, that's the one.
CRI middleware functions (the playback layer)¶
| Function | Address | Role |
|---|---|---|
LuxAudio_GetCriAtomManagerSingleton |
0x140547D60 |
Lazy-init singleton: 0x150-byte CRI manager, thread-once gate at DAT_1441492F0, owner of all loaded .acb sheets and active voice instances. 201 callers — chokepoint. |
LuxAudio_ResolveCueSheetEntryByKey |
0x140547210 |
Sheet entry lookup (critical-section guarded, ref-counted). Sheet table at manager+0x08, count at manager+0x10, lock at manager+0x28. |
LuxAudio_RegisterActiveVoiceInstance |
0x14054F8B0 |
Plays a cue. Generates a non-zero, non-colliding random instance id (rand() — non-deterministic) and inserts it into the player's active-voice TMap. Returns the new instance id, or 0xFFFFFFFF on failure. |
LuxAudio_PlayCueIdByByte |
0x140550900 |
Main byte-cue dispatcher. Looks up active player at manager+0xA8, resolves the active sheet context at manager+0xA0, and submits to LuxAudio_RegisterActiveVoiceInstance. |
LuxAudio_PlayCueIdByByteThunk |
0x1405509A0 |
Trivial thunk; loses the instance-id return value. |
LuxAudio_FireSoundCue_ViaVfxDispatcher |
0x1403110B0 |
Bridge from MoveVM cue scheduler → g_pLuxVfxDispatcher.vt[0x38] → CRI. Useful gate point for burst-playback mitigation. |
DataTable-driven config lookups¶
A family of FName-keyed TMap lookups inside the audio-config UObject.
All share the same probe pattern (FName ctor → FUN_14089BE30 TMap
probe → return +8 payload pointer of match):
| Function | FName key | Holds |
|---|---|---|
LuxAudio_LookupVoiceCueIDConvTable @ 0x140425610 |
VoiceCueIDConvTable |
Generic voice-cue-id → CRI cue id remap |
LuxAudio_LookupDramaticVoiceData @ 0x140422930 |
DramaticVoiceData |
Root table mapping DramaticVoiceID → ULuxDramaticVoiceDataAsset |
LuxAudio_LookupVoiceConvTable_Creation @ 0x140422170 |
Creation_VoiceConvTable |
Character-creation voice picker |
LuxAudio_LookupVoiceConvTable_B5NPC @ 0x140421740 |
B5NPC_VoiceConvTable |
B5-stage NPC voices |
LuxAudio_LookupVoiceConvTable_DV08 @ 0x1404224A0 |
DV08_VoiceConvTable |
Dramatic Voice scenario 08 |
LuxAudio_LookupVoiceConvTable_DV17 @ 0x140422620 |
DV17_VoiceConvTable |
Dramatic Voice scenario 17 |
LuxAudio_LookupVoiceConvTable_DV56 @ 0x1404227A0 |
DV56_VoiceConvTable |
Dramatic Voice scenario 56 |
LuxAudio_LookupSoundBusDackingData @ 0x140423F40 |
SoundBusDackingData |
CRI REACT-DUCKING bus matrix (Dacking = Japanese romaji for "ducking") |
LuxAudio_LookupStageMaterialSoundTable @ 0x1404247B0 |
StageMaterialSoundTable |
Stage-material → foley cue map (concrete / sand / grass / wood / metal …) |
LuxAudio_LookupWeaponBankIdTable @ 0x140425B80 |
WeaponBankIdTable |
(SoulChargeType, bone hash) → weapon-SE bank id |
Practical modding model¶
Keep audio mods split by layer. The game treats "what sound exists", "which runtime id points to it", and "whether a side effect is allowed to fire right now" as separate problems:
| Layer | Change | Good for | Main constraint |
|---|---|---|---|
| Content replacement | Replace an already-loaded .acb with a compatible one at the same routed path. |
One-for-one weapon SE, voice, stage ambience, or foley swaps. | Preserve the cue inventory the game asks for. A missing cue usually becomes silence or a failed play request. |
| Data-asset / table routing | Change a UDataAsset/DataTable row so an existing runtime selector resolves to a different cue, bank, persona, material, or bus config. | Redirecting existing authored triggers without changing code. | The referenced sheet still has to be loaded by an existing loader path. |
| Native hook | Intercept cue submission, sheet lookup, mute/active state, or ducking at runtime. | Conditional replacement, debug logging, pause/fast-forward gating, and rollback-safe side-effect suppression. | Hooks run on the game-thread submission path; CRI playback continues on the audio thread once submitted. |
Dropping a new cue into an .acb does not make SC6 play it by itself. A
trigger still has to reference it through MoveVM event data, a voice
conversion table, a dramatic-voice asset, a stage-material row, a PartsSE
entry, or a native hook. Conversely, if you only want to replace an
existing sound, keep the runtime ids stable and avoid table edits.
This page intentionally does not document an AtomCraft build pipeline, AWB packing recipe, or arbitrary cooked asset paths. Use the routed paths and table names that are documented here unless another page has verified the route.
Cue-sheet / ACB routing checklist¶
Every successful play request needs three things to line up:
- A loaded
.acbsheet in the expected runtime slot. - The sheet key/type the submitting system resolves through
LuxAudio_ResolveCueSheetEntryByKey. - A cue id that exists inside that sheet.
For a practical replacement, identify the source before touching content:
| Source | Routing surface | Practical check |
|---|---|---|
| Move-authored battle SE / voice | MoveVM event record table → event cue scheduler → ESCS_* sheet slot |
Start from the move-side event data. The move-system event table is the authoring-side source of these side effects. |
| Weapon SE | WeaponBankIdTable → CS_%03X_%d.acb loader |
Confirm SoulChargeType, bone hash, and bank id before replacing a file. |
| Character voice | Character voice ACB map + VoiceCueIDConvTable family |
Confirm language (JP/US) and whether the option-bank path is in use. |
| DramaticVoice | DramaticVoiceData row selected by battle-rule DramaticVoiceID |
Add or redirect personas through data only after a trigger path already exists. |
| Stage material foley | StageMaterialSoundTable |
Confirm the stage material key/row before changing footstep or landing sounds. |
| Costume part SE | Costume part asset PartsSE entries |
Confirm the part category and event type; this is separate from character voice and weapon SE. |
| Runtime muting / ducking | UAtomComponent active toggle or SoundBusDackingData |
Use when you want level control, not content replacement. |
The folder path alone is not enough evidence. For example, two systems can load CRI content from nearby folders but submit cues through different sheet slots. Verify the sheet slot, the loader path, and the cue id together.
Weapon SE bank routing¶
LuxObject_GetWeaponBankId_BySoulChargeAndBone @ 0x140425D00 resolves
the bank id used to pick the correct CS_NNN_M.acb. Lookup chain:
- From the chara, deref
+0x20 → sub-object, then+0x1E0(main weapon) or+0x1E8(alt weapon) based on the bone-slot argument. - Read the bone hash byte at descriptor
+0x159(or+0x150+0x28if indirected). FString_Printf("%03X", SoulChargeType)→ FName →WeaponBankIdTablerow.- Read the bank id u32 at
row+0x08+bone_hash*4.
DLC weapon .acb index map¶
LuxObject_CreateAsyncLoader_WeaponSoundACB_RegisterAndAppend @ 0x14042C130
builds the path Sound/_resource/SE/Weapon/CS_%03X_%d.acb (SoulCharge type
in hex, weapon bank id in decimal) and overrides the base folder for DLC
SoulCharge types:
SoulChargeType |
Folder prefix |
|---|---|
0x09 |
DLC/13/Sound/_resource/SE/Weapon/ |
0x17 |
DLC/06/Sound/_resource/SE/Weapon/ |
0x22 |
DLC/11/Sound/_resource/SE/Weapon/ |
0x28 |
DLC/07/Sound/_resource/SE/Weapon/ |
0x30 |
DLC/04/Sound/_resource/SE/Weapon/ |
0x60 |
DLC/01/Sound/_resource/SE/Weapon/ |
0x61 |
DLC/09/Sound/_resource/SE/Weapon/ |
0x13 |
base game, but remapped to 0x11 |
| anything else | base game Sound/_resource/SE/Weapon/ |
Useful when you ship a custom weapon and need its SE bank to load correctly without owning the DLC slot.
Practical weapon-SE replacement¶
Use the least invasive route that matches the goal:
| Goal | Route | Notes |
|---|---|---|
| Replace sounds for an existing weapon bank | Content replacement at the resolved CS_%03X_%d.acb path. |
Keep cue ids compatible with the original bank. This preserves existing MoveVM triggers and per-side sheet routing. |
| Reuse another existing weapon bank for a custom weapon | Data-asset/table edit through WeaponBankIdTable. |
Change the (SoulChargeType, bone hash) row to an already-routed bank id. Do this only where your asset workflow can safely edit the table. |
| Select weapon SE conditionally at runtime | Native hook around LuxObject_GetWeaponBankId_BySoulChargeAndBone or the later cue-submit path. |
Keep P1/P2 sheet slots distinct. A hook that rewrites the final cue id without checking the active sheet can make one side play the other side's bank. |
| Silence a weapon during a tool mode | Mute/active gate, not file replacement. | Use the UAtomComponent active path or the cue-submit gate described below; do not ship a blank .acb unless silence is the mod's intended content. |
Minimum verification for a weapon SE mod:
- Test base state and Soul Charge state.
- Test both players; the sheet enum has separate weapon slots for P1/P2.
- Test the specific bone/weapon variant that owns the bank row.
- Replay the same exchange once; a content swap should not require different runtime behavior in replay viewing.
Per-character voice ACB maps¶
Character voice files follow
Sound/_resource/Voice/{JP,US}/Character_Voice/{J,E}_VO_CV_NNN_OPTION.acb
where NNN is the char id in hex. Most characters use a generic per-id
setup; a few (e.g. char ID 0x60) get their own per-char init function
that inserts the map keys explicitly:
// LuxActor_InitCharacterVoice_Char060_AudioMaps @ 0x14040EB40
// key 0x400004 → "Sound/_resource/Voice/JP/Character_Voice/J_VO_CV_060_OPTION.acb"
// key 0x400005 → "Sound/_resource/Voice/US/Character_Voice/E_VO_CV_060_OPTION.acb"
ULuxCeBankManager (StaticClass accessors at 0x1409A59D0 and
0x1409A92C0, UClass slot at DAT_14414F080) owns the runtime registry
of loaded character-voice and weapon-SE ACBs.
Voice-cue replacement strategy¶
SC6 has several voice families that sound similar in-game but route through different data:
| Voice target | Preferred modding route | What to verify |
|---|---|---|
| Per-move battle grunts / shouts | Replace the character voice .acb or redirect through the appropriate voice conversion table. |
The move still submits the same authored cue id through the event scheduler. |
| JP/US language swap or replacement | Replace the language-specific J_VO_CV_* or E_VO_CV_* ACB that is actually loaded. |
Test both language settings; replacing only one side leaves the other untouched. |
| DramaticVoice persona | Add or redirect a DramaticVoiceData entry and assign the DramaticVoiceID through battle-rule data. |
The trigger enum already contains the moment you want; adding a cue without a trigger will not play it. |
| Creation-mode voice | Route through Creation_VoiceConvTable. |
The TypeSelect/Edit windows choose a parent voice first, then map it to CRI cue ids. |
| Win-pose / pre-round / story lines | Treat as Enshutsu/cinematic voice, not MoveVM battle voice. | Variant counts come from the Enshutsu voice item path documented below. |
For one-for-one voice swaps, keep the cue ids expected by the original bank and only replace content. For new lines, add the data reference that causes an existing trigger to select the cue; an extra cue inside an ACB is inert until a table, asset, move event, or hook references it.
Enshutsu (cinematic) voice subsystem¶
"Enshutsu" (演出 = staging / production) is the cinematic-voice path used for win poses, pre-round banter, and story-mode lines. Distinct from in-battle move audio.
| Function | Address | Role |
|---|---|---|
LuxVoice_AddVoiceItemsForPose |
0x14038D350 |
Reads the per-chara ENST-magic blob at chara+0x28, indexes by (poseId, animType), and emits N voice items. |
LuxVoice_GetItemCountForAnimType |
0x14038D550 |
Nested switch returning 1, 3, or 0 voice-item count for (animType, voiceCategory). |
LuxVoice_EnshutsuHeader_AddVoiceItem |
0x14038BC40 |
Allocates a 32-byte VoiceItem, ref-counts it, and appends to the EnshutsuHeader effect list. |
LuxVoice_AllocItemRefCountWrapper |
0x14038D650 |
24-byte std::_Ref_count<VoiceItem> allocator. |
LuxVoice_EnshutsuHeader_VoiceItem_ScalarNewDtor |
0x14038C470 |
MSVC scalar deleting dtor. |
LuxVoice_RegisterCompiledInScriptStruct_FLuxVoiceTableRawAsset |
0x140174C10 |
UHT-generated FLuxVoiceTableRawAsset UScriptStruct registration. |
The per-(animType, category) variant count that GetItemCountForAnimType
returns determines which categories get triple variants (taunt / hit
reaction / round-win) versus a single deterministic line.
Creation-mode voice editing¶
Two config UObjects:
| Symbol | Address | UClass slot |
|---|---|---|
ULuxCreationVoiceEditWindowConfig |
0x140176790 |
DAT_144150708 |
ULuxCreationVoiceTypeSelectWindowConfig |
0x1401767C0 |
DAT_144150730 |
The TypeSelect window picks a voice parent (shout / scream / panic /
…); the Edit window tunes parameters within the chosen type. Selections
are mapped through Creation_VoiceConvTable to CRI cue ids.
UE-side: UAtomComponent bridge¶
CRI ships an Unreal plugin whose in-world emitter component is
UAtomComponent (NOT the stock UE UAudioComponent). UE owns the
spatialization transform; the AtomComponent forwards the cue submission
to the CRI player. The path from chara to the live component:
LuxAudio_ResolveCharaAudioComponentSlot @ 0x1404265D0:
obj = this->vftable[0x138]() // animSubObj / skeletal-mesh holder
children = *(obj + 0x140) // audioBindings — AttachChildren-like
audioComp = FUN_141E4F210(children, 0) // walk for first child with +0x30 set
return FUN_142049490(audioComp) // checks AtomComponent +0x500
// (active / bAutoActivate)
UAtomComponent::vftable[0xA08] is the mute / active toggle. See
LuxBattleChara_SyncAudioActiveState_FromBattleFlags @ 0x140438980 for
the canonical call site.
Muting and ducking safely¶
Muting is a presentation control, not a simulation pause. Choose the smallest scope that matches the UX:
| Goal | Route | Notes |
|---|---|---|
| Silence one character's emitted voice/SE | UAtomComponent::vftable[0xA08] through LuxAudio_ResolveCharaAudioComponentSlot. |
Affects the live component. It does not rewind or stop already-submitted CRI voices unless the component/player path handles that voice. |
| Force-mute a battle character | Set bit 2 of chara+0x394 and let LuxBattleChara_SyncAudioActiveState_FromBattleFlags drive the component. |
Good for tool modes that already own chara state. Keep bit 0/bit 2 semantics distinct. |
| Suppress new MoveVM-authored sounds during a hidden step | Gate LuxAudio_FireSoundCue_ViaVfxDispatcher @ 0x1403110B0. |
This blocks new cue submissions from the event scheduler but does not silence voices already in flight. |
| Lower one bus under another | Route through SoundBusDackingData. |
The lookup is documented; custom activation policy should be verified against an existing in-game ducking case before relying on a new row. |
| Pause / frame-step inspection | Combine simulation gates with audio gates. | See Battle Manager pause gates; zeroing VM time alone is not an audio stop control. |
For replacement mods, avoid solving volume problems by shipping blank cue content. Blank content changes the asset behavior for every route that loads that sheet, while a mute, duck, or submit gate can be scoped to the mode that needs silence.
Cue sheet types and other audio enums¶
ELuxCueSheetType (sheet key indexed against the manager's sheet table
at manager+0x08):
enum class ELuxCueSheetType : uint32 {
ESCS_CMN = 0,
ESCS_GROUND = 1,
ESCS_BACK_GROUND = 2,
ESCS_FOLEY = 3,
ESCS_PLAYER_ONE_WEAPON = 4,
ESCS_PLAYER_TWO_WEAPON = 5,
ESCS_VO_CMN = 6,
ESCS_PLAYER_ONE = 7,
ESCS_PLAYER_TWO = 8,
ESCS_DRAMATIC_VOICE = 9,
ESCS_PLAYER_ONE_OPTION = 10,
ESCS_PLAYER_TWO_OPTION = 11,
ESCS_MAX = 12,
};
ELuxVoiceBankType — which voice-acb slot a runtime cue resolves to:
enum class ELuxVoiceBankType : uint32 {
EVB_Common = 0,
EVB_Character1P = 1,
EVB_Character2P = 2,
EVB_CharacterOption1P = 3, // DLC/option voice (e.g. char 060)
EVB_CharacterOption2P = 4,
EVB_MAX = 5,
};
ELuxVoiceLanguage — selects between the J_VO_CV_* and E_VO_CV_*
ACB families:
ELuxBattleDramaticVoiceTriggerType is documented above under
DramaticVoice.
Costume-part SE (PartsSE / PartsBreak)¶
Per-costume-part SE is a cue family separate from the chara's main voice and SE: each clothing or equipment part registers its own cue triggers, which fire on motion events (swing, walk, jump landing, …) and on costume-break states. The enums:
enum class ELuxPartsSEType : uint32 {
SwingKick = 0,
Walk = 1,
Run = 2,
Jump = 3,
TouchDown = 4,
Other = 5,
SwingWeapon = 6,
ReversalEdge = 7,
MAX = 8,
};
enum class ELuxPartsSECategory : uint32 {
None = 0,
Waist = 1,
Mantle = 2,
Arm = 3,
Leg = 4,
Accessory = 5,
Footwear = 6,
MAX = 7,
};
enum class ELuxPartsSEAcsType : uint32 { S = 0, L = 1, MAX = 2 };
// small (light cloth) vs large (heavy)
enum class ELuxPartsBreak : uint32 {
NULL_ = 0,
HIGH = 1, // outermost layer broken
MID = 2,
LOW = 3, // innermost / fully destroyed
MAX = 4,
};
Modder relevance: PartsSE is the system to tune when a custom outfit
sounds wrong — silent footsteps, missing cloth swing, no break cue when
the costume shatters. Define the SE triggers in the costume part asset,
referencing the correct ELuxPartsSEType for each animation event.
Stage material foley¶
Stage material foley is the route for footsteps, landings, and similar
surface-dependent sounds. The confirmed lookup is
LuxAudio_LookupStageMaterialSoundTable @ 0x1404247B0; the stage page
tracks nearby stage-audio investigation work under
Stage investigation backlog.
Use this decision tree for custom or replaced stages:
- If the stage should sound like an existing material, keep the material key/row pointed at the existing foley cues and replace only content if the global sound itself should change.
- If the stage needs a different material-to-foley mapping, edit the
StageMaterialSoundTabledata route where your asset workflow can do that safely. - If the stage needs a wholly new foley bank, first prove an existing loader path brings that sheet into the CRI manager. This page does not document an arbitrary new stage-foley ACB path.
- Keep stage collision and stage foley separate. Visible UE material, rollback-safe terrain/wall data, and CRI foley routing are different systems; changing one does not prove the others changed.
Stage background SE / ambience is also separate from material foley. The
known starting point is
LuxObject_CreateAsyncLoader_StageBGSE_ACB_RegisterAndAppend @ 0x14042d3d0,
but the full BGSE path routing is not expanded on this page.
Replay determinism¶
Audio is not part of the deterministic simulation. The simulation itself is bit-exact across replays (frame counter, MoveVM state, hit detection), but the audio output may vary:
| Source | Effect | Affects gameplay? |
|---|---|---|
rand() in LuxAudio_RegisterActiveVoiceInstance |
Instance-handle ids differ per run. | No — handle only used to stop/modify the same voice later. |
rand() in Audio_RandomTick @ 0x140399B70 (when *DAT_1441456F0 > 0) |
Per-frame audio-mix RNG scalar differs. | No — mixer modulation only. |
FPaths_GameLogDir() seed in SelectAndPlaySound_Ranked |
Ranked-match flavor pool pick differs. | No — flavor lines. |
| MoveVM-scripted move cues | Deterministic from MoveVM state. | — |
| DramaticVoice triggers | Deterministic (persona id is in replay header). | — |
Replay viewing keeps the audio thread running normally. See replay-system.md for the world-tick freeze story and why
VMFreezeBytedoes not silence in-flight voices.
Fast-forward / rollback side-effect gating¶
Audio cue submission is a side effect of the simulated frame. That matters when a tool advances hidden frames, drains replay catch-up, or experiments with rollback-style resimulation:
| Situation | Risk | Conservative gate |
|---|---|---|
| Fast-forward / skip | Many frame-authored cue submissions arrive in one visible tick. | Suppress LuxAudio_FireSoundCue_ViaVfxDispatcher during the hidden drain, or mute UAtomComponents before the drain and restore them after. |
| Replay freeze without clock pinning | The replay master clock accumulates delta, then drains multiple sim frames on release. |
Fix the clock gate first; muting only hides the burst. See SimulationLoop catch-up. |
| Rollback prototype hidden resim | Audio from discarded frames can double-play or leak prediction mistakes. | Suppress audio during hidden resim and emit only final visible-frame events where possible. See the rollback side-effect plan. |
| Debug frame-step | Repeated stepping over the same authored event can resubmit the same cue. | Log frame id + chara + sheet + cue id and dedupe in the tool layer if the same visible frame is revisited. |
Do not mutate MoveVM event data just to suppress sound during a tool mode. Gate the presentation side effect instead. The authored move events are also used to reason about VFX and other side effects in the move-system event table.
Runtime verification checklist¶
Verify audio mods by proving which layer changed: content, routing data, or runtime gate. A useful log line at the cue-submit layer should include:
| Field | Why it matters |
|---|---|
| Battle frame / replay master clock | Separates one real event from a replay catch-up or frame-step duplicate. |
| Player side / chara id | Distinguishes P1/P2 sheet slots and mirrored tests. |
| Source family | MoveVM event, weapon SE, voice, DramaticVoice, PartsSE, stage material, ambience, or hook-generated. |
Sheet key / ELuxCueSheetType |
Catches routing to the wrong CRI sheet even when cue ids look plausible. |
| ACB path or bank id | Confirms the loader path and table row selected the expected content. |
| Cue id | Confirms the submitted cue exists in the target sheet. |
| Gate state | Records whether the event was submitted, muted, ducked, suppressed, or deduped. |
Use the failure mode to pick the next check:
| Observation | Likely layer |
|---|---|
No submit reaches LuxAudio_FireSoundCue_ViaVfxDispatcher for an authored move event |
MoveVM event data or trigger timing, not CRI content. |
| Submit reaches the dispatcher with the wrong sheet or bank | Routing table / loader selection issue. |
| Submit reaches the expected sheet and cue but nothing is heard | Missing cue in the replacement ACB, inactive UAtomComponent, bus ducking/mute, or CRI playback failure. |
| Sound plays once in normal play but duplicates during frame-step, replay catch-up, or rollback lab resim | Side-effect gate / dedupe issue. |
| Weapon or voice works for P1 but not P2 | Per-side sheet slot or language/bank map mismatch. |
| Stage foley changes globally instead of only on the custom stage | Content replacement was used where a material-table route was needed. |
Minimum smoke tests for practical mods:
- Normal battle, training pause/frame-step if the tool supports it, and one replay viewing pass.
- P1 and P2, including mirrored character order.
- JP and US voice settings for voice mods.
- Base state and Soul Charge for weapon-SE mods.
- The custom stage material, landing, step, and ambience cases for stage-audio mods.
- Fast-forward or hidden-step path if the mod owns one.
Where to hook for audio mods¶
| Goal | Hook |
|---|---|
| Mute a specific chara | UAtomComponent::vftable[0xA08] on the chara's audio component (via LuxAudio_ResolveCharaAudioComponentSlot @ 0x1404265D0) |
| Mute all in-battle SE/voice | Set bit 2 of chara+0x394 (force-mute) on every chara |
| Gate all CRI cue submission at a single point | Hook LuxAudio_GetCriAtomManagerSingleton @ 0x140547D60 (201 callers) or LuxAudio_FireSoundCue_ViaVfxDispatcher @ 0x1403110B0 (cue-only chokepoint) |
| Mitigate fast-forward burst-playback | Gate body of LuxAudio_FireSoundCue_ViaVfxDispatcher OR toggle UAtomComponent::vftable[0xA08] = false before the burst |
| Replace a weapon SE | Drop in Sound/_resource/SE/Weapon/CS_NNN_M.acb matching the SoulChargeType + bank id (or the DLC-folder variant) |
| Replace a character voice | Drop in Sound/_resource/Voice/{JP,US}/Character_Voice/{J,E}_VO_CV_NNN_OPTION.acb |
| Add a new dramatic voice persona | Add row to the DramaticVoiceData DataTable; assign a new DramaticVoiceID; surface in the battle-rule DataTable as PlayerLeft.DramaticVoiceID |
| Fix custom-costume silence (no footsteps / cloth) | Wire up ELuxPartsSEType cues on each costume part asset; set the right ELuxPartsSECategory |
| Duck the SE/voice bus | Write a SoundBusDackingData config DataTable row targeting the bus you want, then call into the CRI player to activate it |
Function quick reference¶
| Symbol | Address | Page |
|---|---|---|
LuxMoveVM_TriggerAudioPaletteCallback |
0x1403693D0 |
This page (Path A) |
LuxMoveVM_TickCharaEventCueScheduler |
0x14038BD60 |
This page (Path B) |
LuxMoveVM_DispatchEffectOp |
0x140376B20 |
move-system.md |
LuxAudio_PlayCueIdByByte |
0x140550900 |
This page (CRI middleware) |
LuxAudio_RegisterActiveVoiceInstance |
0x14054F8B0 |
This page (CRI middleware) |
LuxAudio_GetCriAtomManagerSingleton |
0x140547D60 |
This page (CRI middleware) |
LuxAudio_ResolveCueSheetEntryByKey |
0x140547210 |
This page (CRI middleware) |
ALuxBattleChara_SelectAndPlaySound_Ranked |
0x1403C8950 |
This page (voice families) |
ResolveDramaticVoiceID_FromBattleSetting |
0x1405F2910 |
This page (DramaticVoice) |
LuxBattleChara_SyncAudioActiveState_FromBattleFlags |
0x140438980 |
This page (chara offsets) |
LuxAudio_ResolveCharaAudioComponentSlot |
0x1404265D0 |
This page (UAtomComponent bridge) |
LuxAudio_FireSoundCue_ViaVfxDispatcher |
0x1403110B0 |
This page (VFX+Audio dispatcher) |
LuxObject_GetWeaponBankId_BySoulChargeAndBone |
0x140425D00 |
This page (weapon SE) |
LuxAudio_CreateAsyncLoader_WeaponSoundACB |
0x14042C130 |
This page (DLC ACB map) |
LuxAudio_InitVoiceAcbMap_Char060 |
0x14040EB40 |
This page (per-character ACB maps) |
LuxAudio_OnCharaPartUnregister_RecordReplayIfActive |
0x140427DF0 |
Replay capture of audio state on chara teardown |
RegisterCompiledInClass_ULuxDramaticVoiceDataAsset |
0x140161AD0 |
This page (DramaticVoice) |
ULuxCeBankManager_StaticClass |
0x1409A59D0 / 0x1409A92C0 |
This page (CeBankManager) |