97 lines
8.8 KiB
Markdown
97 lines
8.8 KiB
Markdown
# 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).
|
|
16. (2026-10-09) The WADs hold only streamed audio. About 15,800 more sounds (footsteps, foley, combat, weapons,
|
|
UI, grunts) are in-memory WEMs inside soundbanks: 65 loose `SFX\*.bnk` (1,117) and 3,340 banks embedded in
|
|
the `.upk` packages (15,641; many banks copied into several packages). None are in the catalog; the WAD
|
|
side is complete. Inventory, layout and editing consequences: `docs/formats/banks.md`.
|
|
17. (2026-10-09) Speech lines have **per-language media IDs**: every package carries one BKPK table per language
|
|
for the same WAD entry. 396 lines (78-80 per language) have a prefetch head (first N bytes) in banks; each
|
|
head equals the start of exactly one language's WEM.
|
|
18. (2026-10-09) In-game test of in-memory sounds (`ackit verify --ingame --banks`): 77 probes written into the
|
|
78 sounds stored in the Frontend*/Startup packages (54 files: patched LZO packages + loose banks). The game
|
|
starts and runs, all 68 probe byte patterns are found in its memory (it loaded the patched packages), revert
|
|
byte-identical: PASS. Not shown: a probe being *heard*. Only 7 Vorbis streams start on the title screen
|
|
(setups 4f708975, 6ded1c65, 92f94a3f, d93b5944, d4b2e824: shared by hundreds of sounds, music included) and
|
|
none is a probe. Tried: Enter/arrow keys posted to the game window (`debugger.post_key`, WM_KEYDOWN) to reach
|
|
the main menu: no UI sound followed (the game probably reads input another way; global SendInput is not
|
|
used on purpose); a breakpoint on the setup parser entry: no extra events. Hypotheses: no in-memory sound
|
|
plays without input; the decode path is the same as for WAD sounds (same WEM rules, 300/300 in-memory WEMs
|
|
rebuild byte-identically), so playback is expected to work like E4/E10.
|
|
19. (2026-10-09) In-game text test (`ackit verify --ingame --texts`, ITA): 12 menu texts of FRONTEND/GFxUI
|
|
prefixed with markers and 12 subtitles patched in place (21 files). All 12 markers are found as UTF-16 in the
|
|
game's memory after ~45 s (the localization files are read at startup), no crash, revert byte-identical:
|
|
PASS. Subtitles are not loaded on the title screen (no dialogue object in Frontend*/Startup/BmGame).
|
|
|
|
## 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`.
|