Wolfenstein Modloader Buy me a coffeeKaffee spendieren

Wiki / Latest research roundsNeueste Forschungsrunden

Sound Replacement Write Path (TNC Wwise Audio)

Partly solvedTeilweise gelöst Original page in the GitHub wikiOriginalseite im GitHub-Wiki

In plain wordsIn einfachen Worten

This report looked at where Wolfenstein II stores its sounds and how to swap one for a new, possibly longer one. The sound packs turn out to be ordinary game archives, so the project's existing archive writer works on them without changes. The replacement should be plain 16-bit PCM audio, because the project cannot encode the game's packed Vorbis format. Two test replacements went through a full rebuild and were read back correctly, but everything happened on copies; nothing was played in the running game.

  • Every sound pack is a normal game archive; no sound-specific writer was needed.
  • Each sound's size is stored twice and matched in all 131,236 shipped cases (0 mismatches); a replacement must update both.
  • Use 16-bit PCM WAVE files; there is no encoder for the game's packed Vorbis audio.
  • A loose and a bank-embedded replacement were rebuilt and re-read correctly; 2 negative controls fired as expected.
  • The New Order does not use Wwise at all; nothing was tested in game.

Dieser Bericht untersuchte, wo Wolfenstein II seine Sounds speichert und wie man einen durch einen neuen, auch längeren ersetzt. Die Soundpakete sind ganz normale Spielarchive, deshalb funktioniert der vorhandene Archiv-Schreiber des Projekts ohne Änderung. Der Ersatz sollte einfaches 16-Bit-PCM-Audio sein, weil das Projekt das gepackte Vorbis-Format des Spiels nicht erzeugen kann. Zwei Test-Ersetzungen liefen durch einen kompletten Neubau und wurden korrekt zurückgelesen, aber alles geschah an Kopien; im laufenden Spiel wurde nichts abgespielt.

  • Jedes Soundpaket ist ein normales Spiel-Archiv; ein eigener Sound-Schreiber war nicht nötig.
  • Die Größe jedes Sounds steht doppelt drin und stimmt in allen 131.236 Originalfällen überein (0 Abweichungen); ein Ersatz muss beide Stellen anpassen.
  • Nutze 16-Bit-PCM-WAVE-Dateien; für das gepackte Vorbis-Audio des Spiels gibt es keinen Encoder.
  • Ein loser und ein in eine Bank eingebetteter Ersatz wurden neu gebaut und korrekt zurückgelesen; 2 Gegenproben schlugen wie erwartet an.
  • The New Order nutzt gar kein Wwise; im Spiel wurde nichts getestet.

Technical detailsTechnische Details

Der technische Teil ist auf Englisch – so, wie er im Wiki steht. Unterstrichene Begriffe haben eine Erklärung auf Deutsch.

Where .wem/.bnk sound media live in The New Colossus, whether the engine checks sizes or hashes on them, and how a bigger replacement would be appended. TNC uses Wwise audio banks; TNO does not use Wwise at all (an exhaustive file search across the whole TNO install finds zero .wem/.bnk/Wwise-named files of any kind — its audio is raw Ogg Vorbis in two monolithic containers, a separate, already-documented system). Every TNC sound pack turns out to be an ordinary archive container, the exact same format already used for textures and decls, so the project's existing generic archive writer applies unmodified — no sound-specific writer had to be built. A full structural round trip (replacing a loose sound and a bank-embedded sound, both forced through a full archive rebuild, plus two negative controls) succeeds end to end and re-reads correctly through the project's own unmodified readers. Everything here was built and torn down on disposable temporary copies; nothing was run in a live game.

Storage — PROVEN

Every sound pack (base/sound/soundbanks/pc/*.pack, and per-DLC equivalents) is an ordinary archive container — same format as the main game archives, entry type "file", compression 0 (stored throughout). The generic archive writer this project already uses for textures/decls/entity definitions applies to sound packs unmodified.

Two kinds of entry inside a pack:

  • a loose top-level <id>.wem entry — the payload is the whole RIFF/WAVE file;
  • a .bnk entry — itself a small container: BKHD (header), DIDX (index: u32 media id, u32 offset, u32 size, 12 bytes/slot), DATA (the WEM slices back to back), HIRC (objects: events, actions, Sound nodes).

No language-id field exists in DIDX itself — language is the pack/folder, not a per-row field.

The load-bearing duplicate-size invariant

Every HIRC Sound object carries its own duplicate u32 size field (byte offset 9 of the object body). Across all 131,236 Sound objects in the full retail+DLC install, wherever the referenced media also has a local DIDX row in the same bank, that row's size and the Sound object's own size field are equal — 0 mismatches. A replacement must patch both the DIDX row's size and the matching HIRC Sound object's own size field, or it reintroduces exactly the inconsistency that never occurs in 131k shipped rows (plausible rejection risk; the actual runtime check itself was not disassembled, no disassembler was available in this research environment).

Container-level checks — PROVEN (pre-existing project findings, re-applied here)

The function every pack/archive file goes through to be mounted unconditionally checks magic, version, the header hash, and the metadata hash on every open. Both hashes are FarmHash64-derived and fully recomputable with the project's existing hash-finalizer — already the production write path for every other asset type this project patches. An optional, off-by-default per-entry payload hash is the same mechanism, nothing Wwise-specific layered on top.

Growth/append rule (PROVEN, round-tripped)

A stored (uncompressed) entry has exactly three outcomes when its payload length changes (measured against a real entry with a real trailing gap):

New length Result
same as old in-place rewrite, nothing else moves
bigger, fits the trailing gap in-place (same mechanics as the already-proven texture-database append trick)
bigger, doesn't fit the gap full archive rebuild — every later entry's offset shifts
smaller, by even 1 byte full rebuild — a stored entry's in-place path requires an exact length match

What a replacement must be — PROVEN by elimination

Two codecs occur in retail: Wwise's packed Vorbis, and PCM. The project can decode packed Wwise Vorbis (strip the custom bit-packing back to stock Ogg Vorbis, using a codebook library extracted from the game executable) but has no encoder — producing a new, correctly packed Vorbis stream would mean writing a full Vorbis encoder constrained to reuse the shipped codebook library bit-for-bit, out of scope for this project. The practical replacement codec is 16-bit PCM: the project's own acceptance rule is a dozen ordinary WAVE-format fields (16-bit, block align = 2×channels, 1–8 channels, 1000–192000 Hz) — a plain RIFF/WAVE file any standard library can build from scratch.

Loose-file loading — does not exist for shipped data

Zero .wem files exist outside a pack anywhere in the install. The decompiled string constants support the same conclusion: the entry-name format strings read as archive-internal names (<lang>/<id>.<ext>), not filesystem paths, and the only related error string is specifically about failing to open a pack file.

Full structural round trip — PROVEN

Both a loose top-level .wem replacement and a bank-embedded (DIDX+HIRC) replacement were built, forced through the expensive rebuild path (both replacements were larger than any available in-place gap), and re-read successfully with the project's own unmodified readers — the re-reader was not told in advance where the new bytes were, finding them the same way a fresh game process would. Two negative controls both correctly fired: leaving the HIRC duplicate-size field stale is caught by the size-mismatch check; flipping one header-hash byte is caught by independent hash re-derivation.

Recipe

  1. Open the pack, find the target entry (<id>.wem directly, or the owning .bnk + that id's DIDX row).
  2. Build the replacement as a 16-bit PCM WAVE file — sidesteps the missing Vorbis encoder entirely.
  3. Loose entry: rewrite its bytes in place through the archive writer. Bank entry: split the .bnk into its four sections, append the new WEM to DATA, rewrite that id's DIDX row, patch the matching HIRC Sound object's own size field, rejoin, and write the rewritten bank back.
  4. Always finish with the project's header-hash finalizer — the same step the existing write path already performs for every other asset type.
  5. Re-verify every slot in the touched bank (not just the changed one) before touching the real install — the regression control that would catch an off-by-one section-length mistake.

Open questions

  • Whether the engine actually enforces the HIRC/DIDX size match at load time, or it just always happens to hold in shipped data — needs either a disassembler or an in-game test.
  • Whether the sound-pack open function goes through the same container-mount hash check as every other archive type — consistent with the identical file format measured, not confirmed by disassembly.
  • TNO's own audio append path (appending a replacement past its current slot in the flat, offset-addressed container) was not round-tripped this pass — structurally analogous to the growth-to-rebuild path proven here, but not yet built or tested.

In-game test (not yet run)

Replace one unobtrusive menu sound effect with a 16-bit PCM replacement built as above (keep the original backed up). Start the game, trigger the sound, and confirm the new audio plays with no "corrupt header" message and no crash. Restore the original and re-trigger to confirm only that one file was affected.