Files
UnheardCity/docs/formats/wem.md

5.1 KiB

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. Checked on every WEM of the 5 speech WADs: 54,079 of 54,080 are identical; the exception (INT FE9963E8:74) differs only in the informational avg-bytes/s field (one lower than the formula below). In SFX 626 of 651 are identical; the others are 14 music tracks whose seek table follows another placement (one entry per 1,024-1,103 samples), 5 music tracks whose sample count exceeds what their packets hold, and 6 4-channel ambience WEMs with a smpl (loop) chunk. These rules are not needed to write sounds the game plays (replaced music, built with the rules here, is heard in game: ackit verify --ingame), only to reproduce those originals.

RIFF layout

RIFF size WAVE, then fmt (0x42 bytes), optional LIST + cue (659 WEMs), LIST + cue + smpl (1 WEM), cue (18 WEMs), cue + smpl (3 WEMs), smpl (41 WEMs), 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.