Initial commit

This commit is contained in:
2026-10-09 02:48:24 +02:00
commit e05d973a8d
133 changed files with 101507 additions and 0 deletions

14
docs/README.md Normal file
View File

@@ -0,0 +1,14 @@
# Documentation
| document | content |
|----------|---------|
| [user-guide.md](user-guide.md) | installing and using Unheard City (screenshots) |
| [decisions.md](decisions.md) | stack, dependencies/licenses, originals store, size rule, trade-offs |
| [categories.md](categories.md) | group/category scheme and character fields |
| [building.md](building.md) | building the editor and its installer (Windows, Linux) |
| [formats/wad.md](formats/wad.md) | WWAD archives, grouped entries, BKPK stream packs |
| [formats/wem.md](formats/wem.md) | WEM layout the game accepts, setup stripping, hash and decode-alloc formulas |
| [reverse-engineering/decoder.md](reverse-engineering/decoder.md) | BatmanAC.exe decoder functions and addresses |
| [reverse-engineering/findings.md](reverse-engineering/findings.md) | root cause, findings, in-game experiment log (incl. failed hypotheses) |
| [../data/catalog/README.md](../data/catalog/README.md) | catalog CSV schema, adding languages |
| [history/](history/) | the two mission briefs this project was built from |

55
docs/building.md Normal file
View File

@@ -0,0 +1,55 @@
# Building Unheard City
The build is a PyInstaller **one-folder** bundle: Python, Qt (incl. Qt Multimedia), libsndfile (libvorbis,
FLAC, opus, mpg123) and soxr are all inside; nothing else is installed or needed at runtime. Build on each
target OS (no cross-compiling).
## Windows (x86-64) - tested
```
py -3.14 -m venv .venv (Python 3.10-3.14)
.venv\Scripts\python -m pip install -e .[app,dev]
.venv\Scripts\python -m pytest -q
.venv\Scripts\python tools\build_app.py
```
That one command does everything, offline. If `app/assets/` (git-ignored icon and wallpaper) is missing, it is
made from `bac_logo_full.png` and `wallpaper.jpg` in `Downloads\Arkham City` (or `--art FOLDER`); without those
images the build still works, with a plain icon and no wallpaper.
Output: `dist\UnheardCity\UnheardCity.exe` and `dist\UnheardCity-<ver>-windows-amd64.zip`
(~78 MB zip, ~190 MB unpacked). The build also writes `THIRD_PARTY_LICENSES.txt` (license texts come from the
wheels and from `tools/licenses/`).
**Installer.** On Windows the folder (and zip) also contains `Install.cmd`, `install.ps1` and `uninstall.ps1`
(sources in `tools/installer/`, version filled in by the build). It is a plain PowerShell installer, so no
installer tool is needed to build it: per-user install to `%LOCALAPPDATA%\Programs\UnheardCity`, Start menu and
optional desktop shortcut, an uninstall entry in Settings > Apps (`HKCU`), upgrade in place. User data in
`%APPDATA%\UnheardCity` is kept on uninstall unless the user chooses otherwise. Unattended:
`install.ps1 -Quiet -Desktop no [-Target DIR]`, `uninstall.ps1 -Quiet`.
Verify the bundle is self-contained (PATH stripped to Windows folders):
```
set PATH=C:\Windows\System32;C:\Windows
dist\UnheardCity\UnheardCity.exe --smoke-test --report smoke.txt & type smoke.txt
dist\UnheardCity\UnheardCity.exe --quit-after 8 --screenshot shot.png
```
## Linux (x86-64) - procedure (not executed on the development machine)
Use the oldest distribution you want to support (glibc compatibility), e.g. Ubuntu 22.04:
```
sudo apt install python3 python3-venv libxcb-cursor0 libxkbcommon-x11-0 libegl1 libpulse0
python3 -m venv .venv && . .venv/bin/activate
pip install -e .[app,dev]
QT_QPA_PLATFORM=offscreen pytest -q
python tools/build_app.py # -> dist/UnheardCity-<ver>-linux-x86_64.tar.gz
QT_QPA_PLATFORM=offscreen dist/UnheardCity/UnheardCity --smoke-test --game /path/to/game
```
Notes:
- The `soundfile` and `soxr` wheels for manylinux bundle libsndfile/libsoxr; the PySide6 wheels bundle Qt and
Qt Multimedia's FFmpeg backend (PulseAudio/PipeWire output). The target desktop only needs the usual X11/
Wayland and PulseAudio/PipeWire client libraries that every desktop distribution has.
- Game folders under Proton are found automatically in `~/.steam/steam`, `~/.local/share/Steam`, Flatpak
(`~/.var/app/com.valvesoftware.Steam/...`) and Snap Steam libraries, including extra libraries from
`libraryfolders.vdf`. The game language is read from the Proton prefix
(`steamapps/compatdata/200260/pfx/drive_c/users/steamuser/Documents/WB Games/...`).
- The in-game test harness (`ackit verify --ingame`) is Windows-only; everything else is portable.
A GitHub Actions workflow that builds both platforms is in `.github/workflows/build.yml`.

64
docs/categories.md Normal file
View File

@@ -0,0 +1,64 @@
# Categories and characters
Implemented in `ackit/classify.py`; used both when building the shipped catalog (`tools/build_catalog.py`)
and when a game build is not in the catalog (runtime scan), so every language/build gets the same scheme.
## Groups and categories
Every sound has a **group** and a finer **category**, derived from its Wwise name (from the community hash
list; unnamed IDs show as hex). First matching rule wins:
| name starts with | group | category |
|------------------|-------|----------|
| `CHR_RadioScanner`, `PSO_CarRadio`, `Speech_RADIO`, `Speech_TV`, `Speech_CAMERAS`, `DIA_` | Speech | Radio & TV |
| `Speech_TAPE_` | Speech | Interview tapes |
| `Speech_INTERROGATION_` | Speech | Interrogations |
| `Speech_CINEMATIC_`, `Speech_New_` | Speech | Cinematics |
| `Speech_Boss_` / `Speech_BOSS_` | Speech | Boss fights |
| `Speech_RIDDLER_`, `Speech_RIDDLES_` | Speech | Riddler |
| `Speech_SIDE_` | Speech | Side missions |
| `Speech_DLC_` | Speech | DLC |
| `Speech_PRED_` | Speech | Predator chatter |
| `Speech_COMBAT_`, `Speech_Bully_` | Speech | Combat barks |
| `Speech_EMOTE_` | Speech | Efforts & emotes |
| `Speech_REACT_` | Speech | Reactions |
| `Speech_STREETS_`, `Speech_ZONES_`, `Speech_OUTCAST_`, `Speech_Vertical_` | Speech | Open-world chatter |
| `Speech_CHAR_` | Speech | Character lines |
| `Speech_CH_`, `Speech_SEQUENCE_`, `Speech_STEEL_`, `_MUSEUM_`, `_CHURCH_`, `_COURT_`, `_UNDER_`, `_GCPD_`, `_ZP1_` | Speech | Story missions |
| `Speech_GAME_`, `Speech_MAP_`, `Speech_AUGMENTED_`, `Speech_FORENSICS_`, `Speech_TEMP_` | Speech | Gameplay & hints |
| `NPC_` | Ambience | Crowds |
| other `Speech_` | Speech | Other speech |
| media of the `Music` / `Music_PDLC` stream packs, `Music*` | Music | Music / Music (DLC) |
| `AMB_`, `PSA_` | Ambience | Ambience |
| `WND_` | Ambience | Wind |
| `VOL_`, `PVOL_` | Ambience | Room tone |
| `SEQ_` | SFX | Scripted sequences |
| `OBJ_` | SFX | Objects |
| `WPN_` | SFX | Weapons |
| `CHR_` (in SFX) | SFX | Character foley |
| `Global` | SFX | Global |
| anything else (e.g. unnamed hex IDs) | Unknown | Unknown |
Counts in the ITA catalog: Story missions 1993, Predator chatter 1796, Character lines 1537, Open-world chatter
1295, Side missions 808, Combat barks 801, Efforts & emotes 616, Interview tapes 514, Riddler 360, Radio & TV 226,
Gameplay & hints 199, DLC 167, Boss fights 150, Cinematics 150, Reactions 108, Interrogations 95. SFX archive:
Music 389, Ambience 225, SFX 31, Unknown 6.
## Characters
Two separate fields, because Wwise names describe a *scene*, not the speaker of each variation (a conversation
alternates speakers between `NAME_1`, `NAME_2`, ...):
- **character** - the verified speaker, only from the curated lists in `data/catalog/source/<lang>/*.txt`
(one file per character, listening-based). Mapped by sound name, so labels made on the Italian files apply to
every language (the five language WADs have identical names and variation counts). Display names:
Calendar -> Calendar Man, Croc -> Killer Croc, Freeze -> Mr. Freeze, Ivy -> Poison Ivy, Madhatter -> Mad
Hatter, Quinn -> Harley Quinn, Ras -> Ra's al Ghul, Strange -> Hugo Strange, Talia -> Talia al Ghul,
Twoface -> Two-Face, Vicky -> Vicki Vale, Zsazz -> Victor Zsasz, Unknown -> Unknown (speaker checked but not
identified). A line in two lists gets both ("A; B").
- **characters_inferred** - characters and roles *mentioned by the name* (token rules in `classify._TOKENS`,
e.g. `BM`/`BATMAN` -> Batman, `PENG` -> Penguin, `TF` -> Two-Face, `THUG*`/`GENTHUG*` -> Thug, `TYGER` ->
TYGER guard; interview tapes add Hugo Strange). A hint for the ~7,500 lines without a verified speaker,
never shown as the speaker. In the editor the character filter matches verified speakers, and optionally
inferred ones.
Coverage (every language): 3,366 sounds with a verified speaker, 5,930 more with inferred characters, 1,520 with
neither (mostly predator/efforts from unnamed thugs).

107
docs/decisions.md Normal file
View File

@@ -0,0 +1,107 @@
# Decisions
## 1. Stack: Python + Qt (PySide6), packaged with PyInstaller
- **Reuse:** the toolkit (`ackit`) is Python and proven against the real game; the editor imports it unchanged.
- **Cross-platform GUI:** Qt Widgets via PySide6 (LGPL-3.0, official Qt for Python) looks native on
Windows and Linux, has a fast model/view table for 11,000+ rows, and runs offscreen for automated GUI tests.
- **Audio I/O:** Qt Multimedia (`QMediaDevices`, `QAudioSink`, `QAudioSource`) for playback, recording and
device selection. It ships inside the PySide6 wheels with its own backends (WASAPI on Windows,
PulseAudio/PipeWire on Linux), so unlike PortAudio/`sounddevice` nothing has to be installed on Linux.
- **Distribution:** PyInstaller one-folder build per platform. It bundles Python, Qt and all native libraries;
the user unzips and runs `UnheardCity(.exe)`, or on Windows double-clicks `Install.cmd` (see below). Nothing
else is installed. Alternatives considered:
Tauri/Rust or C# would mean rewriting the verified codec/injector (high risk, no benefit); a local web UI
would still need a bundled runtime and makes audio recording harder.
## 2. No external programs at runtime
The first toolkit used ffmpeg (decoding inputs and libvorbis encoding) and vgmstream (checks). Both are gone:
- **Encoding:** `libsndfile` (via the `soundfile` wheel) contains libvorbis 1.3.7. Tested: it yields exactly the
same codebook compatibility and the same game setups as ffmpeg's libvorbis (findings 12). Encoding is in-process.
- **Decoding game WEMs:** ackit's own WEM -> Ogg converter (the game's codebooks) + libsndfile.
- **Input formats:** libsndfile reads WAV, FLAC, OGG/Vorbis/Opus, MP3, AIFF and more. M4A/AAC/WMA would need
ffmpeg: an ffmpeg found on the system is used if present, otherwise the user gets a clear "convert to WAV/MP3"
message. Bundling ffmpeg (~80-100 MB, GPL/LGPL build care) for rare formats was judged not worth it.
- **Resampling:** `soxr` (LGPL-2.1).
So there are **no vendor binaries to fetch** (no `tools/fetch_vendor` script was needed): every native
dependency comes from version-pinned PyPI wheels (which embed libsndfile, libvorbis, libogg, FLAC, opus, mpg123,
Qt and Qt Multimedia's FFmpeg backend) that PyInstaller bundles, resolved relative to the app (never `PATH`).
`tools/collect_licenses.py` writes `THIRD_PARTY_LICENSES.txt` into each build from the wheels' own license files
plus official texts kept in `tools/licenses/` (sources in SOURCES.txt; no network at build time). Verified: the packaged Windows build runs
its decode/encode/inject smoke test with `PATH` reduced to `C:\Windows` (no ffmpeg, vgmstream or Python).
**Licenses:** PySide6/Qt LGPL-3.0 (dynamic linking, unmodified Qt shared libraries in the one-folder build,
source at https://code.qt.io), libsndfile LGPL-2.1, mpg123 LGPL-2.1, soxr LGPL-2.1, libvorbis/libogg/FLAC/opus
BSD, numpy BSD, Python PSF. One-folder (not one-file) builds keep the LGPL libraries replaceable.
The only Windows-only part is the in-game test harness (`ackit verify --ingame`, uses WinDbg/cdb and WASAPI
loopback); it is a developer tool and not part of the editor.
## 3. Where originals live (revert safety)
- **Originals store** per game folder: `<user data>/UnheardCity/stores/<id>/` (Windows `%APPDATA%`, Linux
`~/.local/share/unheardcity`), id = hash of the game path; configurable (`--store`, editor settings).
Before a sound is first changed, its original WEM bytes (SHA-256-named blob) and metadata are saved;
the applied replacement is saved too. **Revert** re-injects the stored original; **Revert all** does this for
every record. Stream-pack padding bytes are stored so pack reverts are byte-identical.
- **Full-file fallback:** before the first change to a WAD (and its pack index), one verified full copy is kept
in the store (`full/`), default on, can be disabled. Used by "Restore from full backup" if the store is damaged.
- **Why the user data folder:** it survives app updates and reinstalls, needs no write access next to the game,
and works the same on Linux/Proton. Steam never touches it.
- **Safe writes:** every WAD is rebuilt into a temp file and atomically renamed over the original, pack indexes
first. Several sounds of one WAD are written in one pass. A locked WAD (game running) gives a clear message.
- **Changed underneath:** each sound's state is computed from its bytes: original / modified (ours) / lost
(ours was replaced by the original, e.g. Steam "verify") / changed (neither: update or another tool) /
foreign (never touched by us, but not the catalog's original checksum). Writing over changed/foreign sounds
requires explicit confirmation; the catalog is used only when the WAD fingerprint matches.
## 4. Size rule
Speech/ambience variations are addressed by offset+size from BKPK tables inside compressed `.upk` packages,
which ackit does not rewrite. Replacements are written in place and must not exceed the original WEM's size;
the encoder lowers quality, then sample rate, to fit, and refuses otherwise. Music stream packs have a loose
index (`SFX\*.bnk`) that ackit rewrites, so music can be any length. (Rewriting `.upk` packages would lift the
limit but means LZO recompression of large engine packages: out of scope, high risk.)
## 5. Catalog
- Shipped CSVs per archive (names, categories, characters, original size + checksum) make loading instant and
enable change detection; any other build is scanned at runtime (header reads only, cached).
- Verified speakers come only from the user's lists; name-based character inference is kept in a separate field.
- Category scheme: `docs/categories.md`.
## 6. Repository cleanup choices
- Windows installer: a per-user PowerShell installer shipped in the zip (`Install.cmd`) instead of Inno Setup or
MSI, so building needs no extra tool; no admin rights, Start menu shortcut, Settings > Apps uninstall entry.
The zip still runs without installing.
- One entry point: `ackit` (CLI) and `UnheardCity` (GUI). The `.bat` launchers and the `mods/` folder
flow were removed (`ackit apply <folder>` and mod packages replace them).
- The user's third-party downloads that sat in `tools/` and `repositories/` (quickbms, Ravioli, AbdAshes' guide,
vgmstream/ww2ogg/revorb sources) were moved to the git-ignored `external/` folder, since `tools/` now holds
build scripts only.
- Experiment scripts and outputs were deleted; they are preserved in git tag `pre-cleanup`.
- The old `_backup/` (2.3 GB full copies) was deleted on 2026-10-08 with the game verified original; the originals
store does not need it.
## 7. Editor UX choices
- One window: filters (left), sound table (top), editor of the selected sound (bottom). The primary flow
(search -> select -> Space to listen -> Load file/Record -> Apply) needs no menus; power features (tags,
favorites, multi-select revert, mod packages, settings) are in the toolbar and context menu.
- Long work (loading, encoding, writing 0.5 GB WADs) runs in background jobs with status messages; the
window stays responsive. Errors are shown as plain sentences (game running/locked file, wrong folder,
unsupported audio, too long, changed outside the tool).
- No network access and no telemetry. User notes/tags/favorites live in the user data folder.
- One look: neutral dark greys (no blue tint), a single dark red accent (#b0242c, theme and icon), solid panels. Icons are vector drawings in
`app/icons.py` (no emoji, no icon fonts, no image files), so they look the same on every platform.
- **Artwork:** the app icon (the Arkham City logo bat in flat black over a red audio waveform of rounded bars, on a
white rounded tile; shown only in the window/taskbar and the toolbar corner) and the default background wallpaper come from images supplied by the user.
`tools/make_assets.py <folder>` turns `bac_logo_full.png` + `wallpaper.jpg` into `app/assets/` (app_icon.png,
app_icon.ico, wallpaper.jpg), which the build bundles. The folder is git-ignored because it is Warner Bros.
artwork: keep builds that contain it for personal use. Without the assets the app falls back to a plain
waveform icon and no wallpaper.
- Background: the wallpaper sits only behind the filter panel, scaled to the panel height so its left part fills
the panel, under a light veil. Settings -> Background toggles it and sets its strength (default 10 %).
- Loading feedback stays where the work is, never modal: a spinner inside the waveform being loaded (sound or
replacement file, decoded off the GUI thread), next to the Apply/Revert buttons while encoding/writing, and in
the status bar for game loading and bulk changes.
All filter selectors (including Character) are plain, non-editable drop-downs so they look and behave alike.
## 8. Testing limitations
- No Linux machine, WSL distribution or container runtime is available on the development PC. Linux support is
covered by portable code (no Windows-only calls outside `ackit/verify`), path handling for Steam/Flatpak/
Proton, unit tests that run on any OS, and a documented build procedure; the Linux package could not be
executed here.

70
docs/formats/wad.md Normal file
View File

@@ -0,0 +1,70 @@
# Arkham City WAD / BKPK container format
All values little-endian. Verified by byte-identical round trips of every shipped WAD
(INT/ITA/FRA/DEU/ESN LOC WADs and `SFX\17705D3E.wad`) and every BKPK pack.
## WWAD (`*.wad`)
| off | type | meaning |
|------|------|---------|
| 0x00 | 4s | `WWAD` |
| 0x04 | u32 | version `0x01020000` |
| 0x08 | u32 | entry count N |
| 0x0C | u32 | table offset (always `0x1C`) |
| 0x10 | u32 | table size (`12*N`) |
| 0x14 | u32 | data start (header area size; `0x8000` LOC, `0x1000` SFX) |
| 0x18 | u32 | data size (`file size - data start`) |
| 0x1C | N x {u32 hash, u32 size, u32 offset} | |
Rules:
- Table sorted by `hash` interpreted as **signed** int32. Payloads stored in table order.
- Each payload starts at the next **0x1000-aligned** offset after the previous one.
- `size` = payload length rounded up to **0x800**; padding is zero.
- File ends 0x1000-aligned; header area after the table is zero.
- `hash` = Wwise media/file ID (names: `hashlist.txt`, e.g. `CHR_RadioScanner`, `AMB_Steel_B3`, `Music`).
Payload kinds:
- **LOC WADs**: each entry is a group of WEMs, the variations of one line (ITA: 10816 WEMs in
2464 entries; e.g. `CHR_RadioScanner` holds 91). Each WEM starts at the next 0x800 boundary after
the previous one (zero padding); 3 entries pack two WEMs back to back. Addressed `HASH:i`.
- **SFX WAD**: 79 grouped ambience/effect entries (260 WEMs) plus 4 **stream packs** (below).
Where the game gets the inner offsets: the entry IDs appear next to `BKPK` headers inside the
LZO-compressed UE3 packages (e.g. `BmGame.upk`, `OW.upk`, `Church_B1_*.upk`). Those embedded BKPK
tables give each WEM's offset **and size**. ackit does not rewrite `.upk` files, so these WEMs are
replaced **in place, no larger than the original** (zero-filled to the original size), and the WAD
layout never changes. The user's earlier quickbms-reimport mod did the same and was loaded by the game.
Only the 4 SFX stream packs, whose BKPK index is a loose `.bnk`, can change size.
## BKPK stream packs (`SFX\<ID>.bnk` + SFX WAD entry `<ID>`)
`EDF036D6` (Music, 314 WEMs, incl. the intro/front-end music `393BC53A`), `E8F980BE` (Music_PDLC, 75),
`57572DAC` (Global, 1), `FCA4F778` (1). The `.bnk` file is an index + an embedded soundbank; the WEM
data lives in the SFX WAD entry with the same ID.
| off | meaning |
|------|---------|
| 0x00 | `BKPK` |
| 0x04 | version `0x01020000` |
| 0x08 | pack id (== WAD entry hash) |
| 0x10 | media count N |
| 0x18 | table size `12*N` |
| 0x1C | table offset `0x48` |
| 0x20 | total stream size (== WAD entry size) |
| 0x2C | end of table (`0x48 + 12*N`) |
| 0x38 | embedded bank size |
| 0x3C | embedded bank offset (0x800 or 0x1000; a normal BKHD/HIRC/STID bank) |
| 0x48 | N x {u32 media id, u32 size, u32 offset in the WAD entry} |
Rules: table sorted by media id; each media starts at the next **0x800-aligned** offset; total =
end rounded up to 0x800. Padding is mostly zero (a few non-zero bytes in `EDF036D6`; the tool keeps
original bytes where nothing moved).
Replacing a pack media (`ackit` target `EDF036D6/393BC53A`) rewrites the pack table (`.bnk`), the
WAD entry payload, and the WAD table/offsets. Verified in-game (NOTES.md, E2).
## Regular soundbanks (`SFX\*.bnk`, bank version 56)
BKHD / DIDX {id, offset, size} / DATA / HIRC / STID. In-memory WEMs (DATA) are 16-byte aligned.
`ackit/bnk.py` can read/rebuild them, but most front-end sounds come from banks embedded in UE3
packages (`.upk`), which this toolkit does not modify.

82
docs/formats/wem.md Normal file
View File

@@ -0,0 +1,82 @@
# WEM format accepted by Arkham City (Wwise 2011.1, Vorbis)
Derived from the game's WEMs; `ackit verify` / `tests/test_realgame.py` rebuild them byte-for-byte from these rules
alone (all 10816 ITA WEMs; in SFX only the 4 looping music tracks with loop points differ).
## RIFF layout
`RIFF` size `WAVE`, then `fmt ` (0x42 bytes), optional `cue `/`LIST` (37 files), `data`.
Odd-sized `data` chunks are **not** padded.
### `fmt ` (0x42)
| off | type | value |
|-----|------|-------|
| 0x00 | u16 | 0xFFFF (Wwise Vorbis) |
| 0x02 | u16 | channels (1,2,3,4,5,6 seen) |
| 0x04 | u32 | sample rate |
| 0x08 | u32 | avg bytes/s = `(data_len - seek_size) * rate // total_frames` |
| 0x0C | u16,u16 | 0, 0 |
| 0x10 | u16 | cbSize 0x30 |
| 0x12 | u16 | valid bits (16 or 24 = source bit depth, informational) |
| 0x14 | u32 | channel mask (mono 0x4, stereo 0x3, quad 0x33, 5.0 0x37, 5.1 0x3F) |
| 0x18 | 42 B | AkVorbisInfo (below) |
### AkVorbisInfo (fmt+0x18)
| off | type | field | rule |
|-----|------|-------|------|
| 0x00 | u32 | dwTotalPCMFrames | sample count |
| 0x04 | u32 | dwLoopStartPacketOffset | `2 + setup_len` (relative to end of seek table) |
| 0x08 | u32 | dwLoopEndPacketOffset | `data_len - seek_size` |
| 0x0C | u16 | uLoopBeginExtra | 0 |
| 0x0E | u16 | uLoopEndExtra | = uLastGranuleExtra |
| 0x10 | u32 | dwSeekTableSize | bytes (4 per entry) |
| 0x14 | u32 | dwVorbisDataOffset | `seek_size + 2 + setup_len` |
| 0x18 | u16 | uMaxPacketSize | max audio packet payload |
| 0x1A | u16 | uLastGranuleExtra | `sum(packet samples) - total_frames` (< blocksize) |
| 0x1C | u32 | dwDecodeAllocSize | see "decode alloc" — **must be right** (too small = NULL write crash) |
| 0x20 | u32 | dwDecodeX64AllocSize | `alloc + 8*nbooks + 40 + 52*nfloors` (fit; unused by 32-bit game) |
| 0x24 | u32 | uHashCodebook | **FNV-1 32** of the stripped setup bytes; codec-setup cache key |
| 0x28 | u8,u8 | uBlockSizes | log2 short, log2 long |
## `data` chunk
```
[seek table: N x {u16 granule delta, u16 byte delta}]
[u16 setup_len][stripped setup packet]
[u16 len][audio packet] ...
```
- Seek points: walk packets; point at the first packet whose *end* granule >= previous point + interval
(8192 for SFX/speech; music uses 1024 or other per-sound values). Byte offsets are relative to the
setup start (end of seek table).
- No identification/comment headers (rebuilt from fmt).
## Stripped setup ("external codebooks")
Standard Vorbis setup with: no `\x05vorbis` header; codebook count (8b), then each codebook as a
**10-bit ID** into the built-in library; no time-domain section; floor type field removed (always 1);
residue type 2 bits; mapping type removed; mode window/transform type removed. See
`ackit/wwvorbis.py:transcode_setup` (bidirectional).
Codebook library: the game's table (runtime `0x0158E748`, 598 books) is byte-identical to ww2ogg's
standard `packed_codebooks.bin` (`ackit/data/game_codebooks.bin`). **Wwise 2011.2.x uses the aoTuV 6.03
library instead** — same IDs, different books — which is why files from the newer encoder decode to
garbage/silence in this game.
Stock libvorbis (1.3.7, in ffmpeg and in the libsndfile wheels ackit uses) setups map onto this library for all tested rates with 1/2/4/5
channels, and for 6 channels up to 32 kHz (its 5.1 books at 44.1/48 kHz are newer than the game).
At 24 kHz q>=5 needs a book not in the library. The encoder probes and picks a compatible quality.
## Audio packets
- Blocksizes differ (e.g. 256/2048): "modified" packets — packet-type bit and the long-window
prev/next flags are removed (mode number stays first).
- Equal blocksizes (512/512, 1024/1024): standard Vorbis packets (type bit kept).
- 2-byte size header, no granule.
## Decode alloc (dwDecodeAllocSize)
`0x30 + parser allocations + 4*nbooks + 8`, every allocation rounded up to 4 bytes. Parser
allocations (Tremor-lowmem derived, `ackit/allocsize.py`):
- codebooks: `nbooks*0x3C` + per book: decode table (`used<2` or nodeb==4: `used*8+4`, else
`align4((used*(leafw+1)-2)*nodeb)`) + for dec_type 2 the quantized values (`align4(quantvals)` or
`align4(2*quantvals)`).
- floors: `nfloors*0x24` + per floor `a4(parts) + a4(11*(maxclass+1)) + a4(2*(posts+2)) + a4(posts+2) + 2*a4(posts)`.
- residues: `nres*0x1C` + per residue `a4(ncls) + 8*ncls`.
- mappings: `nmaps*0x14` + `a4(2*coupling_steps)` + `a4(channels)` if submaps>1 + `a4(2*submaps)`.
- modes: `a4(2*nmodes)`.
Exact on all 24 setups the game ships; in-game test E10 used a new setup with computed values.

View File

@@ -0,0 +1,92 @@
# MISSION: Arkham City audio modding toolchain (autonomous)
You are working fully autonomously. I will not answer questions or intervene. When something is ambiguous, pick the most reasonable option, record the assumption in `NOTES.md`, and continue. Never stop to ask me for permission or clarification. Stop only when a success criterion below is met, when a stop condition is hit, or when usage limits force a pause (see "Limits").
## Goal
Make it possible to replace audio in **Batman: Arkham City (PC)** with custom audio in a simple, repeatable way. Deliverables:
1. **Encoder tool**: converts an arbitrary WAV/OGG/MP3 into a WEM file the game actually accepts and plays (correct RIFF/Wwise chunks, Vorbis stream and codebooks as the game's Wwise version expects).
2. **Injector tool**: replaces a given WEM inside the game's WAD container, tracking file sizes and alignment, and updating all offset/size tables so the container stays valid. Must be reversible (restore from backup).
3. **A short README** documenting the WAD structure, WEM format findings, and usage, so a non-expert can replace a sound with one command.
Success = a custom sound I supply plays correctly in-game in place of the original, with no crash, verified by your harness (see "Verification").
## What is already known (do not redo this work)
- Game was built with **Audiokinetic Wwise 2011.1.0**, which is unavailable. The closest available is **2011.2.2** (Internet Archive). Audio is a custom Wwise RIFF (`RIFF/WAVE`, `fmt`, `data`, etc.) containing Vorbis with **stripped headers and packed codebooks**.
- Differences between 2011.1.0 and 2011.2.x exist, including **which Vorbis codebooks the encoder uses**. Header layout differences are also possible and not fully mapped.
- Existing public tools (unpackers/repackers) work for extracting and repacking, but the resulting audio is incompatible: the game refuses to play it or crashes. Alternative codecs (PCM, etc.) were already tried and do not work.
- I already made a **patch that stops the crash**: a code cave that patches the variable holding the number of bytes to allocate on a **bump allocator**. Re-derive details from my patch/notes in the repo if present; do not assume it is complete.
- The game executable has **DRM/packing**: many functions are missing when the original exe is analyzed statically. A **WinDbg memory dump of the running executable** exposes them. Use the dumped exe for Ghidra analysis; use the live process for dynamic analysis.
- Useful references: **vgmstream** and **ww2ogg** (Wwise RIFF parsing, packed codebook handling, codebook files such as `packed_codebooks.bin` / aoTuV variants), **revorb**, and Wwise 2011.2.2 itself as a reference encoder.
Search the filesystem first for my existing patches, dumps, notes, and tools. Treat anything you find as input data, not as ground truth; verify before relying on it.
## Two approaches (pursue in this order, switch when evidence says so)
**A. Make the files compatible with the game.** Reverse engineer the game's WEM loader/Vorbis decoder to find exactly what it requires (header fields, codebook set/IDs, setup-packet format, packet/granule layout, block sizes, seek table, etc.). Then build an encoder that produces that format, likely by encoding with Wwise 2011.2.2 or libvorbis and rewriting headers/codebooks to match the game's expectations. Compare original game WEMs against Wwise 2011.2.2 output to find the differences.
**B. Patch the game binary** to accept more encodings (for example, make the decoder accept the 2011.2.x codebooks or headers, or add a path that supports another codec). Use this if A proves infeasible, or as a complement if it is simpler.
Decide by evidence. Document the reasoning in `NOTES.md`.
## Environment and tools
- Native Windows. Work in a git repo; commit often with meaningful messages.
- Ghidra via MCP (static analysis, on the dumped exe).
- **Steam-only game.** The game is installed through **Steam** and can only be run through Steam (Steam DRM/ownership check). Steam must be running and logged in. Launching the game exe directly may fail or exit; do not rely on it. Consequences you must handle:
- Find the install folder (`steamapps\common\...`) and the game's `appmanifest_*.acf` to get the real App ID and exe name; do not guess them.
- Launch the game via Steam (for example `steam://rungameid/<appid>` or `steam.exe -applaunch <appid>`), or via a Steam **launch option** that wraps the game with the debugger (`<debugger command> %command%`), so the Steam environment is inherited. Test which approach works and record it in `NOTES.md`. Use whichever allows attaching early enough to break during startup and audio loading.
- **Never use Steam's "Verify integrity of game files"** and make sure automatic updates are off for this game, or Steam will overwrite the modified files. Your restore script, not Steam, is the way to restore.
- Keep the backup of the game folder outside the Steam library, and write the restore script to work while Steam is running.
- **Debugger (dynamic analysis): WinDbg is the baseline.** WinDbg is installed and is the default for dynamic analysis. First detect how it is installed (Store version `windbgx.exe`, install folder, any WinDbg MCP tool in your tool list) and record it in `NOTES.md`. Prefer a WinDbg MCP tool if one is connected; otherwise drive WinDbg from the command line with a startup command string, a log file, and debugger scripts (verify the switches actually work with the installed version before building the harness around them). It must run unattended: log to a file, set breakpoints/commands at startup, and terminate the debuggee and debugger on its own at the end. Attaching to the Steam-launched process (by name or PID) is a valid approach. Prefer scripted debugging with log files over interactive tool calls when it is cheaper for context.
- **You are free to improve the workflow.** Any tool, library, script, or technique that speeds up or de-risks the modding process is welcome: other debuggers or tracing tools, API/file-access monitors (for example Process Monitor or Frida-style hooks), in-process hooks or injected DLLs that log or call the game's audio functions directly, emulation or unit-testing of the decoder in isolation, custom Ghidra scripts, format-fuzzing harnesses, or anything else you judge useful. Install what you need, keep installs minimal, and record each addition and why in `NOTES.md`.
- Python for tooling. Available/installable: vgmstream-cli, ww2ogg, revorb, ffmpeg, libvorbis/oggenc, and Wwise 2011.2.2. Check what is installed; if a tool is missing and the network allows, install it; otherwise write a minimal replacement.
- You may debug the running game autonomously: launch it, attach debuggers, set breakpoints, read memory, and kill it. Always work on a copy-restorable game directory.
## Rules of engagement
- **Back up first.** Before modifying anything in the game directory or WAD, create a verified backup and a one-command restore script. Never edit the only copy of anything. Every test run starts from a clean restore plus exactly one change.
- Keep the game's original files byte-identical except for the intended injections.
- Change one variable at a time when testing hypotheses, and log the result.
- Prefer small, falsifiable experiments over long speculation. State a hypothesis, test it, record the outcome.
- Do not spend effort on unrelated goals (graphics, gameplay, other codecs) unless needed.
## Verification (build this first)
Before deep reverse engineering, build an **automated test harness** script that: restores a clean game copy, injects a given WEM, launches the game through Steam with WinDbg attached and logging, triggers or loads the target sound in a deterministic way (for example, a save or a scripted entry point that plays it early), detects crash / hang / clean exit, captures debugger logs and exception info, and prints a PASS/FAIL summary.
Make "does it play" machine-checkable without a human listening. For example: breakpoint on the decoder's init/decode functions and log return values or error codes, check that decoded sample counts match expectations, and optionally capture audio output (loopback) and compare against the source with a similarity metric. Choose whatever is most reliable and document it. Also use vgmstream/ffmpeg decoding as an offline sanity check, remembering that offline success does not imply the game accepts the file.
## Suggested phases
1. Inventory: find existing work, tools, dumps, and notes. Set up the repo, `CLAUDE.md`, `NOTES.md`, backups, and restore script.
2. Reverse the **WAD container**: header, file table, offsets, sizes, alignment, compression/encryption if any. Write a parser and a round-trip test (unpack, repack, byte-identical output).
3. Build the test harness.
4. Reverse the **WEM loading and Vorbis decoding path** in the dumped exe (and live process). Document exact format requirements.
5. Compare original game WEMs with Wwise 2011.2.2 output and map every difference. Test hypotheses with the harness.
6. Implement the encoder (approach A) or binary patch (approach B), whichever the evidence supports.
7. Implement the injector with size/alignment/offset handling, including the case where the new file is larger or smaller than the original.
8. Final end-to-end test with a custom sound, then write the README.
## Memory and context management
- `NOTES.md` is your persistent memory. After every meaningful finding or failed experiment, append: what you tried, the result, and the conclusion. Keep a short "Current status / Next steps" section at the top, rewritten as you go.
- Record addresses, struct layouts, function names, and format specs in a `docs/` folder as you discover them, so you never have to rediscover them after a context reset.
- At the start of every session or after a context reset, read `NOTES.md` first and continue from there.
- Use subagents for bulk or mechanical work (large searches, log triage, batch experiments) to keep the main context small.
## Limits and efficiency
I am on a limited Claude Pro quota. Be economical: avoid dumping huge outputs into context (filter, grep, summarize logs), do not re-read large files unnecessarily, and delegate mechanical work to cheaper subagents. If you hit a usage limit, write a clear status in `NOTES.md` so work can resume cleanly.
## Stop conditions
- **Success:** the end-to-end test passes and the deliverables exist.
- **Dead end:** if approach A and B have each had at least five distinct, well-documented failed hypotheses, stop and write a `REPORT.md` that lists what was proven, what was ruled out, the best remaining hypotheses, and the exact next experiments.
- If any action risks something irreversible outside the project directory and the game directory, do not do it.
## Output at the end
Summarize in a few lines: what works, how to use the tools, and what remains. Everything else lives in the repo.

View File

@@ -0,0 +1,109 @@
# MISSION: Clean up the repo, then build the Arkham City Audio Editor (autonomous)
You are working fully autonomously, exactly as in the previous mission. I will not answer questions or intervene. When something is ambiguous, pick the most reasonable option, record the assumption and reasoning in `NOTES.md`, and continue. Never stop to ask for permission or clarification. Stop only when the success criteria below are met, when a stop condition is hit, or when usage limits force a pause.
## Context
The previous mission succeeded: you reverse engineered the WAD container and the WEM format of **Batman: Arkham City (PC, Steam)**, built an encoder and an injector (the "ackit" tooling, in Python), and patched the game so modified audio plays. All of that is in this repository, together with many experiment scripts, notes, and session-resume markdown files.
Start by reading `NOTES.md`, `CLAUDE.md`, and the docs to re-learn the current state. Do not redo reverse engineering. The Steam rules still apply: the game runs only through Steam, never use "Verify integrity of game files", keep auto-updates off for this game, keep backups outside the Steam library, and keep game files byte-identical except for intended changes. WinDbg and your other tools remain available if you need to verify something.
## Part 1: Repository cleanup (do this first, then commit)
Goal: a small, clear repository where a newcomer can find everything.
1. **Safety net first.** Before deleting anything, commit the current state and create a git tag (for example `pre-cleanup`) so every deleted file is recoverable from history. Do not rewrite history.
2. **Delete** every script and file that is not needed any more: one-off experiments, scratch analysis scripts, dead ends, duplicated tools, temporary outputs, large generated artifacts. Judge by whether the file is part of the final working pipeline or documents it.
3. **Keep:**
- `ackit` (the final toolkit), with only the code that matters: encoding, injection, WAD/WEM parsing, patching, and testing/verification. Merge duplicates, remove leftover and unused code, give modules clear names, and add short docstrings. Keep the tests, make sure they pass, and add tests where core behavior is uncovered (WAD round-trip, WEM encode/decode check, inject/revert).
- **All documentation you wrote**: format specs, address maps, reverse-engineering notes, findings, failed hypotheses worth remembering. Move them into a tidy `docs/` structure, but do not delete or condense away information.
- **The markdown files used to resume sessions** (`NOTES.md`, `CLAUDE.md`, and similar). Keep them, update them to describe the new layout, and keep a short "Current status / Next steps" section.
4. **Simplify usage.** Provide one clear entry point for the toolkit, for example a single CLI (`ackit <command>`) with a few well-named subcommands (such as `info`, `list`, `extract`, `encode`, `inject`, `revert`, `verify`) and helpful `--help` text. Replace the multi-step manual procedures with one-command workflows where possible. Write a short, clear top-level `README.md`: what it is, install, quick start with 2-3 examples, and where things live.
5. **Reorganize the layout** so each kind of thing has an obvious place. Suggested (adapt if you find better):
```
ackit/ Python package (core library + CLI)
tests/ automated tests
data/catalog/ CSV catalogs (one per language/WAD, see below)
docs/ format specs, reverse-engineering notes, how-tos
app/ the editor application (Part 2)
tools/ build/packaging helper scripts only
NOTES.md, CLAUDE.md, README.md
```
6. **Catalog.** My curated catalog of audio files (by character, mainly for the ITA language, though the idea applies to other language WADs too) is the source for names/characters. You had started converting it into CSV files. Locate the original catalog files and your CSV work. Finish converting everything into well-structured CSVs under `data/catalog/`, with a documented schema (see Part 2) and a short `data/catalog/README.md` explaining columns and how to add other languages. Do not lose or overwrite my original catalog files; keep them in a clearly named `data/catalog/source/` folder (unless they are too large or clearly not redistributable, in that case document where they are).
7. Add a `.gitignore` so game assets, extracted audio, dumps, and build outputs never get committed. Do not commit any Arkham City game audio.
8. Run the test suite and one real end-to-end check (inject a custom sound, confirm it plays in game through the existing harness if still available, and revert) to prove the cleanup did not break anything. Commit.
## Part 2: The Audio Editor application
Build a simple yet powerful desktop editor for the game's audio, working on **Windows and Linux** (on Linux the game runs through Steam/Proton, so the editor must accept any game folder path and handle Proton library paths; the editor itself does not need to launch the game).
### Technology decision (you decide, document it in `docs/decisions.md`)
Existing tools are Python. Pick the stack that best balances: working on Windows and Linux, easy installation for non-technical users, good audio playback/recording, reuse of `ackit`, and reasonable effort. Candidates: Python with a Qt GUI (PySide6) packaged with PyInstaller/Nuitka (bundles its own Python, no separate install), a web UI served locally and shown in a window, or a rewrite in another language (for example Rust/Tauri or C#). Prefer reusing `ackit` unchanged over rewriting it, unless there is a strong reason. Requirements for the decision:
- Distribution must not require the user to install Python or other runtimes (bundle what is needed), or else provide a clearly simple one-step installer.
- The encoder must work **without Wwise** at runtime and on Linux as well as Windows. If any part of the encoding pipeline depends on a Windows-only tool, replace that part with portable code, or ship the required pre-generated data (such as codebook data) with the app. Verify on both platforms as far as you can (you can test Linux behavior under WSL or a Linux container if available).
- Audio I/O must be portable (for example `sounddevice`/PortAudio, `miniaudio`, or Qt Multimedia), with device selection.
- **Bundle external tooling in the distribution.** If the app or `ackit` needs any external tool or binary (for example ffmpeg/ffprobe for decoding and converting, vgmstream-cli for decoding game WEMs, revorb, ww2ogg, oggenc/libvorbis, PortAudio or other native libraries), include it in the final dist build for each platform, so the user installs nothing else. Rules:
- Prefer removing a dependency when a small portable library does the job (for example a Python decoder instead of a full ffmpeg), to keep the package small. Otherwise bundle the binary.
- Ship per-platform binaries (Windows and Linux, x86-64 at least) in a `vendor/` or `bin/` folder inside the package, and resolve them at runtime relative to the app (never rely on `PATH`). Fall back to a system copy only if the bundled one is missing.
- Download them from official/trusted sources in a build script (`tools/fetch_vendor.*`), pin versions, and verify checksums. Do not commit large binaries to git; fetch them at build time.
- **Licenses:** check each tool's license (for example ffmpeg LGPL/GPL build variants; prefer an LGPL build without GPL-only components if sufficient). Include a `THIRD_PARTY_LICENSES` file with each license text, version, and source URL, and satisfy redistribution terms (for example, point to the source). Record the decisions in `docs/decisions.md`.
- Test the packaged build on a machine/container **without** these tools on `PATH` to prove it is self-contained.
### Feature 1: Browser
- Load a game folder (and its WADs), remember the last path, and show a table of all audio entries with fast loading even for tens of thousands of entries (lazy loading, indexing/caching in a user cache folder; do not decode everything up front).
- **Search** by name/ID, plus **filters** for category, character, language/WAD, and any other useful field. Combine them freely, with instant results as you type.
- **Categories:** you decide a sensible category scheme (for example Speech, Music, SFX, Ambience, UI, Combat, Cinematic...). Derive them from the catalog and from the data itself (WAD/folder names, Wwise bank/event names, file size/duration heuristics, naming patterns), and document the scheme and the rules in `docs/`. Entries that cannot be classified go into an "Unknown" category and must remain searchable.
- **Characters** come from my catalog CSVs. Entries not in the catalog still appear, with empty character.
- Add other columns/filters you judge useful (duration, size, sample rate, channels, modified/original status, catalog notes or transcript text if present in the catalog, favorites/tags). Sorting on every column, multi-select, and a "modified only" filter.
- Quick preview playback directly from the list (play/stop, keyboard-driven navigation), with a visible indicator for entries modified by the user.
- Let the user add their own notes/tags per entry, stored in a user data file (not in the game folder).
### Feature 2: Single-file editor
Opening an entry shows an editor view with:
- **Listen**: play original and current (modified) audio, waveform display, seek, volume, loop, and A/B compare between original and replacement.
- **Substitute**: replace the audio from a file (WAV, OGG, MP3, FLAC, and whatever else ffmpeg or the chosen libs decode easily). Handle sample-rate/channel conversion, loudness/volume adjustment, and trimming. Warn clearly if the replacement is much longer/larger than the original, and make sure the injector handles any size correctly (offsets and alignment already solved in ackit; reuse it).
- **Record**: record in the moment from a user-selected **audio input device** (device list, level meter, record/stop/re-record, preview before accepting, optional trim and normalize). Test recording logic with a virtual or file-based input where no hardware exists.
- **Apply / Revert**: applying encodes with ackit and injects into the game WAD (with the safety rules from before: back up before first write, atomic/safe writes, never leave the WAD in a half-written state). **Revert** must restore the original audio of that entry, and there must also be "Revert all". Decide where originals live and justify it in `docs/decisions.md`. A good default: the first time an entry is modified, store its original bytes (with hash and location metadata) in an `originals` store; revert re-injects from it, so no full-WAD copy is required. Consider also keeping a pristine backup of the whole WAD as a fallback. Choose the location (user data folder vs. next to the game files) by what is most convenient and portable, for example a per-game-folder store with a configurable path, and make it survive app updates. Detect when the game files changed underneath (for example after a Steam update or verification) and warn the user safely instead of corrupting anything.
- **Export/import mods:** let the user export their changes as a portable package (replacement audio plus a manifest) and import it on another machine or share it, so mods are not tied to one WAD state. Keep it simple.
### UX and quality bar
- Simple first: the primary flow (search, select, listen, replace/record, apply) must be obvious without reading docs. Power features stay available but unobtrusive.
- Clear messages for errors (wrong game version, missing patch, unsupported audio, WAD locked by a running game, Steam path issues).
- Do not add telemetry or network access, except where strictly needed (none expected).
- Keep dependencies reasonable and pinned; keep the app responsive (long operations in background threads with progress and cancel).
## Verification (build it as you go)
- Unit/integration tests for the library; headless tests for the app logic (search/filter index, catalog loading, apply/revert, mod export/import). Use fixtures or synthetic WADs where real game data cannot be committed.
- Automated GUI smoke tests (for example headless/offscreen Qt or a browser driver) that open the app, search, open an entry, replace audio from a file, apply, and revert, checking byte-level restoration of the original.
- At least one **real end-to-end test against the actual game** using the existing harness: apply a replacement through the editor code path, confirm the game still starts and plays it, then revert and confirm the WAD is byte-identical to the original.
- Build the distributable packages for the platform you are on, and verify as far as possible that the Linux package starts and works (container or WSL). Document exactly how to build for the other platform.
## Deliverables
1. Cleaned repository with the structure above, passing tests, and updated `NOTES.md`/`CLAUDE.md`/docs.
2. Simplified `ackit` CLI and top-level `README.md`.
3. Catalog CSVs and schema documentation.
4. The editor app source in `app/`, with build scripts in `tools/`, plus a self-contained packaged build for the current platform (bundled external tools included, with `THIRD_PARTY_LICENSES`) and instructions to build for Windows and Linux.
5. `docs/decisions.md` (stack, category scheme, originals store, trade-offs) and a user guide with a few screenshots (headless screenshots are fine).
## Working rules (unchanged from before)
- `NOTES.md` is your persistent memory: log progress, decisions, and failed attempts; keep "Current status / Next steps" at the top. After a context reset or a usage pause, read it first and continue.
- Commit often with meaningful messages. Back up before touching game files, and test on restorable copies.
- Be economical with quota: filter and summarize outputs, avoid re-reading large files, delegate mechanical or bulk work to subagents, and work in stages (cleanup first, then library changes, then browser, then editor, then packaging) so partial progress is always usable.
- Order of priority if time or quota is short: (1) cleanup, (2) browser with search and filters, (3) single-file editor with substitute and revert, (4) recording, (5) mod export/import, (6) packaging polish. Never sacrifice the revert safety for features.
## Stop conditions
- **Success:** all deliverables exist and the verification tests pass, including the real game end-to-end test.
- **Blocked:** if a requirement cannot be met (for example, the encoder truly needs a Windows-only component), implement the best alternative, document the limitation clearly in `docs/decisions.md` and `README.md`, and continue with the rest. Write `REPORT.md` at the end with what works, what does not, and the next steps.
- Do not take irreversible actions outside the project and game folders.
## Output at the end
A few lines: what was cleaned up, what the app can do, how to run and build it, and known limitations. Everything else lives in the repo.

BIN
docs/img/browse.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

BIN
docs/img/editor.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
docs/img/main.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

BIN
docs/img/settings.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View 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`.

View 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`.

88
docs/user-guide.md Normal file
View File

@@ -0,0 +1,88 @@
# Unheard City - user guide
Replace voice lines, music and ambience in **Batman: Arkham City (PC, Steam)**. Everything you change can be put
back with one click.
## Install and start
1. **Windows:** unzip the download and double-click **Install.cmd**. It installs Unheard City for your user (no
admin rights), adds it to the Start menu (and, if you want, the desktop) and offers to start it. Running a
newer version's Install.cmd later upgrades it. To remove it: Settings > Apps > Installed apps > Unheard City.
You can also skip installing and run `UnheardCity.exe` from the unzipped folder.
**Linux:** unpack the download and run `UnheardCity`.
2. The game is found through Steam automatically. If not (other drive, Linux/Proton), a red bar appears at
the top: click **Choose game folder…** and pick the folder that contains `BmGame` and `Binaries`
(Linux: usually `~/.steam/steam/steamapps/common/Batman Arkham City GOTY`).
3. **Close the game** while you apply or revert changes (the files are locked while it runs).
![Main window](img/main.png)
## Find a sound
- Type in the **search box** (Ctrl+F): words can be parts of the name, a category, a character, your own
notes or tags, or an ID like `b49cdf48`. All words must match.
- Narrow down with **Group** (Speech, Music, Ambience, SFX), **Category** (e.g. *Interview tapes*,
*Combat barks*, *Cinematics*), **Character**, **Archive** (speech language or SFX), **Tag**, **Modified
only** and **Favorites only**. Combine them freely; results update instantly.
- **Character** is the verified speaker from the curated catalog. Grey `~` names are only *mentioned* in the
sound's name (useful hints for the many unlabelled lines); tick *Include characters mentioned in names*
to filter on them too.
- Click a column header to sort (length, size, status, ...). Ctrl/Shift-click selects several rows.
- **Language** (top left) switches the speech language. Speech is read from the language the game is set to.
![Browsing by category](img/browse.png)
## Listen
Select a row and press **Space** (or double-click) to play the original. In the editor below:
- **Original** / **Installed** play the game's original and what is currently in the game. Click on a
waveform to play from that point. **Loop**, **Volume** and **Stop** (Esc) work for everything.
- **A/B** switches between the original and your replacement at the same position, to compare.
## Replace
![Editor](img/editor.png)
1. **Load file…** (Ctrl+O): WAV, FLAC, OGG, OPUS, MP3 or AIFF. (M4A/WMA work only if ffmpeg is installed.)
Or choose a microphone in the list and press **Record**, then **Stop recording**. Listen with
**Replacement**; record again as often as you like.
2. Adjust: **Keep from / to** trims (or right-drag on the replacement waveform), **Trim silence**, **Gain**,
**Normalize**, **Match original loudness** (recommended for voice lines).
3. **Apply to game** (Ctrl+S). The audio is converted to the original's format and written into the game files.
The row turns bold and shows *Modified*.
Size rule: voice lines and effects must fit into the original's size (the *Max size* column), because the
game reads exactly that many bytes. Longer audio is encoded at lower quality to fit; if it still does not fit
you are asked to shorten it. Music (`Music_…`) can be any length. A warning appears when your audio is longer
than the original, since the game may cut it off.
## Undo
- **Revert to original** in the editor puts that sound back exactly.
- Right-click rows → **Revert selected**; toolbar **Revert all** puts every original back.
- Originals are kept in your user data folder (`%APPDATA%\UnheardCity` on Windows,
`~/.local/share/unheardcity` on Linux), so reverting works after updating or reinstalling the app too.
Uninstalling keeps this folder; use **Revert all** before uninstalling if you want the game back to original. Before the first
change to an audio archive, one full copy of it is also kept there as a fallback (Settings).
- If Steam "Verify integrity of game files" or an update undoes your changes, a bar offers **Re-apply my
changes** or **Forget them**. Do not use "verify" to undo mods; use Revert all.
## Notes, tags, favorites
Right-click → **Notes & tags…** (F2): free text and comma-separated tags, searchable and filterable.
Click the **★** column to mark favorites. These are stored in your user data folder, not in the game.
## Share your changes (mods)
- **Export mod…**: choose which changed sounds to include and save a `.acmod` file (the exact game data plus
the audio, so it also works for another language whose original has a different size).
- **Import mod…**: shows which sounds match this game, then installs them. You can revert them like any change.
## Settings
![Settings](img/settings.png)
Game folder, where originals are kept, the full-backup option and the playback device. The recording device
is chosen next to the Record button. **Background**: show or hide the Arkham City wallpaper behind the filter
panel; **Background strength**: how visible it is (10 % by default).
The editor panel opens tall enough to show all its controls; drag the divider above it to change that.
## Troubleshooting
| message | what to do |
|---------|-----------|
| "Cannot write … close the game" | quit Arkham City (and anything else using the file) and try again |
| "not a Batman: Arkham City folder" | pick the folder that contains `BmGame` and `Binaries` |
| "audio is too long for this sound" | shorten the audio (see the original's length) |
| "Cannot read … Supported: WAV, FLAC, …" | convert the file to WAV/MP3/FLAC, or install ffmpeg |
| "changed outside this tool" | the game files were changed by an update or another tool; revert or confirm to overwrite |
| no sound when playing | choose another playback device in Settings |