Initial commit
This commit is contained in:
39
docs/reverse-engineering/decoder.md
Normal file
39
docs/reverse-engineering/decoder.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# BatmanAC.exe — Wwise Vorbis decoder notes (runtime addresses)
|
||||
|
||||
Image base 0x400000 (no ASLR). The on-disk exe is SteamStub-packed; `.text` is decrypted at runtime
|
||||
(within ~0.15 s of process start). Static analysis uses the user's memory dump
|
||||
`ArkhamCityModding\Audio\RE\BatmanAC_dump.bin` (a *mapped* image: file offset == RVA).
|
||||
The Ghidra project `BAC` loaded it with raw-file section mapping, so **Ghidra address = runtime + 0xC00
|
||||
in `.text`** (+0x1E00 in `.data`). All addresses below are runtime.
|
||||
|
||||
| runtime | Ghidra | what |
|
||||
|---------|--------|------|
|
||||
| 0x010B2340 | 0x010B2F40 | stream codec state machine (reads setup across stream buffers, creates setup) |
|
||||
| 0x010B2C30 | 0x010B3830 | in-memory (bank) codec init |
|
||||
| 0x010B3400 | 0x010B4000 | `GetOrCreateCodecSetup(cache, AkSrcInfo*, ...)`: cache bucket `hash % 31`, key = full hash |
|
||||
| 0x010B347B | 0x010B407B | `mov ebx,[ebp+0x94]` = decode alloc size (user's crash patch site; decryption probe) |
|
||||
| 0x010B351A | 0x010B411A | return from setup parse (`eax`=0 OK) — harness breakpoint |
|
||||
| 0x010B3160 | 0x010B3D60 | per-packet synthesis (mode read as **1 bit**) |
|
||||
| 0x010B3600 | 0x010B4200 | decode loop |
|
||||
| 0x010B48F0 | 0x010B54F0 | mapping inverse |
|
||||
| 0x010B4D10 | 0x010B5910 | `ParseSetupHeader(info, channels, reader, pool)` |
|
||||
| 0x010B5140 | 0x010B5D40 | floor1 unpack |
|
||||
| 0x010B5A60 | 0x010B6660 | residue unpack |
|
||||
| 0x010B46D0 | 0x010B52D0 | mapping unpack |
|
||||
| 0x010B5C00 | 0x010B6800 | residue inverse |
|
||||
| 0x010B6340 | 0x010B6F40 | `_make_decode_table` (user called it WwiseDecodeVorbisToPCM) |
|
||||
| 0x010B66F0 | 0x010B72F0 | codebook unpack (Tremor-lowmem `vorbis_book_unpack`, compact Wwise form) |
|
||||
| 0x0158E748 | (.data) | codebook pointer table, 599 ptrs (598 books + end) |
|
||||
|
||||
`AkSrcInfo` (arg 2 of GetOrCreate): +0x6C channel mask, +0x78.. AkVorbisInfo copy (+0x94 decode alloc,
|
||||
+0x98 x64 alloc, +0x9C hash, +0xA0/+0xA1 blocksizes, +0xA4 rate). total_frames reads 0 there.
|
||||
|
||||
Pool: one bump allocator per codec setup, capacity = dwDecodeAllocSize; first 0x30 bytes = setup info;
|
||||
allocation failures return NULL and are not checked -> the old crash (`mov [ebx+eax*2],cx`, ebx=0).
|
||||
The user's patch (double capacity) masked a too-small value produced by Wwise 2011.2.2.
|
||||
|
||||
## Harness hooks (ackit/verify/debugger.py, used by `ackit verify --ingame`)
|
||||
- bp 0x010B3400: log hash/alloc of every stream/media setup (`ACKIT_SETUP`).
|
||||
- bp 0x010B351A: log parse result (`ACKIT_PARSE ret=`).
|
||||
- `sxd -c2` on AV: second-chance crash -> `ACKIT_CRASH` + registers + stack.
|
||||
- WinDbg Store path changes on auto-update: cdb is located via `Get-AppxPackage Microsoft.WinDbg`.
|
||||
75
docs/reverse-engineering/findings.md
Normal file
75
docs/reverse-engineering/findings.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Findings and experiment log
|
||||
|
||||
Moved here from NOTES.md during the 2026-10-08 cleanup (content unchanged; paths of the time are kept,
|
||||
see the mapping at the end). Format specs: `docs/formats/`. Decoder addresses: `decoder.md`.
|
||||
|
||||
## Root cause of the original "silent audio / crash" problem
|
||||
- Wwise 2011.2.2 writes codebook IDs into the *aoTuV 6.03* codebook library; the game (Wwise 2011.1) only has
|
||||
the *standard* library (== ww2ogg `packed_codebooks.bin`). Same IDs -> different books -> garbage/silence.
|
||||
- 2011.2.2 also computes `dwDecodeAllocSize` for a different decoder -> the game's bump allocator overflows ->
|
||||
NULL write crash (the user's old code-cave patch doubled the pool to hide it).
|
||||
- Approach A (chosen and verified): encode with stock libvorbis, transcode to the 2011.1 layout with the game's
|
||||
codebook IDs, compute the hash (FNV-1) and decode-alloc size with the reversed formula. **No exe patch needed.**
|
||||
|
||||
## In-game experiments (2026-10-07; harness then `scripts/harness.py`, now `ackit verify --ingame`)
|
||||
Target SFX:EDF036D6/393BC53A = intro/title music, plays ~5 s after launch without input.
|
||||
|
||||
| # | injected | result |
|
||||
|---|----------|--------|
|
||||
| E0 | baseline (nothing) | PASS ncc 0.974 (detector hears original music) |
|
||||
| E1 | my encode of pink noise+sine, 6ch 32k q2 (maxpkt 1448, 41 KB/s) | setup parsed OK, no crash, but SILENT (ncc 0.117) |
|
||||
| E2 | original game WEM 0D5E8A4E (6ch 48k, other size) | PASS ncc 0.998 -> WAD+BKPK pack injection works in-game |
|
||||
| E3 | original game WEM 8D6D9BB9 (4ch 32k, setup d93b5944 = same as E1) | PASS ncc 0.933 -> rate/channel change accepted |
|
||||
| E4 | my encode of decoded 8D6D9BB9 audio (4ch 32k q2, maxpkt 630) | **PASS ncc 0.959** -> encoder output plays correctly |
|
||||
| E5 | my noise probe 4ch 32k q2 (maxpkt 720), 10 s | SILENT -> not channel count / packet size |
|
||||
| E6 | my encode of Joker speech 4ch 32k q2, 15 s | PASS ncc 0.998 |
|
||||
| E7 | E5 at -20 dB, 10 s | SILENT -> not amplitude |
|
||||
| E8 | E5 low-passed 3 kHz, 10 s | SILENT -> not spectrum |
|
||||
| E9 | same noise probe, **30 s** | **PASS ncc 0.995** -> failures were duration: this music slot does not play sources <= ~10 s (music-segment timing in HIRC), not a codec issue |
|
||||
| E10 | Joker speech 30 s, **mono 28 kHz q4, NEW setup** (hash 04681ac8 via FNV-1, alloc 18180 via formula) | **PASS ncc 1.000** -> computed hash/alloc accepted by the game |
|
||||
| E11 | boot smoke test: ITA entry 800BE4F0 grown 30 KB -> 231 KB (all later entries shifted) + SFX pack change | game boots and plays (PASS). Later found unsafe in principle (see 10.) and now refused |
|
||||
| E12 | `ackit replace SFX EDF036D6/393BC53A custom_sound.mp3` then harness --installed | PASS ncc 0.999 |
|
||||
| E13 | mods-folder state (ITA line Speech_TAPE_JOKER_1_4 in place + music) | PASS ncc 0.999 |
|
||||
| E14 | 2026-10-08, after the cleanup: `ackit verify --ingame` (Workspace.apply path, libsndfile encoder, 6ch 32k probe) | **PASS ncc 1.000, no crash, revert byte-identical (WAD + pack index)** |
|
||||
|
||||
Conclusion: approach A works end-to-end in-game. Use >= 30 s probes for the intro-music slot.
|
||||
|
||||
## Findings log
|
||||
1. WAD: see docs/formats/wad.md. Round trip byte-identical (ITA, INT, SFX). Entries in LOC/SFX WADs are raw RIFF WEMs
|
||||
(3 ITA entries contain 2 concatenated RIFFs).
|
||||
2. Codebooks: game table @ runtime 0x0158E748 = 599 ptrs (598 books) == ww2ogg standard packed_codebooks.bin byte-for-byte.
|
||||
Saved as ackit/data/game_codebooks.bin.
|
||||
3. User's mod.wem (2011.2.2): codebook ids 8-19, 97-107 — every one maps to a different book in std vs aoTuV lib -> wrong decode.
|
||||
Also 1024/1024 blocksizes, decode_alloc 16452.
|
||||
4. Original game WEMs: libvorbis (ffmpeg) at 24k mono q<=4.5 reproduces the game's 24k/9-10 setup byte-for-byte
|
||||
(same 35 codebook ids). Several other known setups also reproduced (see work/qsearch). 28k/44.1k not exactly
|
||||
reproduced (encoder version/tuning) -> handled by formulas instead.
|
||||
5. uHashCodebook = FNV-1 32 over stripped setup bytes (24/24). Codec setup cache keyed by this hash
|
||||
(VorbisGetOrCreateCodecSetup: bucket hash%31, compares full hash) -> distinct setups MUST have distinct hashes.
|
||||
6. dwDecodeAllocSize = 0x30 + parser pool allocations (Tremor-lowmem derived; ackit/allocsize.py) + 4*nbooks + 8. Exact 24/24.
|
||||
7. Field rules (docs/formats/wem.md): last_granule_extra = loop_end_extra = sum(packet samples) - total_frames;
|
||||
loop_start = 2+setup_len; loop_end = data_len - seek_size; avg_bps = (data_len-seek)*rate//frames;
|
||||
seek table = (dgranule,doffset) u16 pairs at first packet whose end-granule >= last+interval (8192 default).
|
||||
8. libvorbis q>=5 at 24k uses a codebook (dims=2, entries=100) absent from the game library -> encoder must pick q that fits.
|
||||
9. Equal blocksizes (9/9, 10/10): packets keep the standard Vorbis packet-type bit; unequal: "modified" packets.
|
||||
10. (2026-10-07, session 2) LOC/SFX entries are groups of variations (ITA 10816 WEMs in 2464 entries). The first
|
||||
splitter only saw the first WEM, so the first injector would have DROPPED variations (fixed). Inner offsets and
|
||||
sizes come from BKPK tables inside compressed .upk packages (entry IDs found next to 'BKPK' in BmGame.upk,
|
||||
OW.upk, ...), so replacements are in place and <= the original WEM size; only SFX stream packs (index in a loose
|
||||
.bnk) may change size. The user's earlier quickbms reimport mod followed the same rule.
|
||||
11. Front-end sounds heard at startup mostly come from banks embedded in .upk packages; the intro music is the only
|
||||
deterministic WAD stream (pack EDF036D6 media 393BC53A, 6ch 48 kHz, 171 s).
|
||||
12. (2026-10-08) libsndfile 1.2.2's bundled libvorbis gives exactly the same compatibility table and the same game
|
||||
setups as ffmpeg's libvorbis (q0..10 at 12/24/28/32/44.1/48 kHz, 1/2/6 ch), so ffmpeg is not needed. libsndfile
|
||||
cannot do q<0 (fit-to-size stops at q0, then lowers the rate).
|
||||
13. (2026-10-08) libsndfile's Vorbis writer crashes with a C stack overflow on a single very large write
|
||||
(30 s x 6 ch); `encoder.vorbis_ogg` writes in 4096-frame blocks.
|
||||
14. libvorbis 6-channel at 44.1/48 kHz uses dedicated 5.1 codebooks (libvorbis 1.3) the game does not have;
|
||||
6 channels work at <= 32 kHz (uncoupled, like Wwise). The encoder falls back to 32 kHz automatically.
|
||||
15. Channel order for > 2 channels: the bitstream order equals the array order given to libsndfile; whether the game
|
||||
expects WAVEFORMATEX order (FL FR FC LFE BL BR) is unverified (identical-channel probes were used).
|
||||
|
||||
## Old path -> new path
|
||||
`scripts/harness.py` -> `ackit verify --ingame` (ackit/verify/ingame.py); `scripts/validate_builder.py` ->
|
||||
`ackit/verify/selfcheck.py` + tests/test_realgame.py; `ackit/gamefiles.py` -> `ackit/game.py` + `ackit/originals.py`;
|
||||
`ackit/simple.py` -> `ackit/workspace.py`; `work/*.py` experiment scripts -> git tag `pre-cleanup`.
|
||||
Reference in New Issue
Block a user