Wolfenstein Modloader Buy me a coffeeKaffee spendieren

Wiki / Latest research roundsNeueste Forschungsrunden

TNO Sound Replacement: The Write Path, Round-Tripped

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

In plain wordsIn einfachen Worten

This report works out every step needed to swap a sound in Wolfenstein: The New Order (TNO): the small sound description in the archive, the pointer to where the audio sits in the big streamed sound file, and the Ogg Vorbis audio itself. All steps were proven on copies: a bullet-impact sound and a voice line were replaced with bigger audio from other game sounds, and a brand-new 1.5-second beep made with ffmpeg fit in the same way. Neither the archive nor the sound file has a checksum; the only one is Ogg's own per-page check, which a valid stream already carries. Nothing was written to the game install and nothing was tested in the running game, and the loader cannot yet apply or undo such changes by itself.

  • Each sound has a 74-byte description; because it is binary it does not always fit its old place (51 to 51 bytes in one case, 59 to 60 in the other), so a full rebuild must be the fallback.
  • The audio container has no index of its own: new audio is simply appended at the end and the sound's pointer is changed; nothing else has to move.
  • Voice lines need no special handling; they are normal sound entries stored in english.streamed instead of streamed.resources.
  • All checks passed for the impact-sound case, the voice-line case and the ffmpeg case, including deliberate damage tests that were correctly caught.
  • Open: whether the game really plays the new audio, what the unconfirmed hash/ID field means, and adding backup and undo for the two sound containers to the loader.

Dieser Bericht erarbeitet jeden Schritt, um in Wolfenstein: The New Order (TNO) einen Sound auszutauschen: die kleine Sound-Beschreibung im Archiv, den Zeiger auf die Stelle in der großen Streaming-Sounddatei und die Ogg-Vorbis-Audiodaten selbst. Alle Schritte wurden an Kopien bewiesen: Ein Einschlagsound und eine Sprachzeile wurden durch größere Audiodaten anderer Spielsounds ersetzt, und auch ein ganz neuer, mit ffmpeg erzeugter Piepton von 1,5 Sekunden passte auf dieselbe Weise hinein. Weder das Archiv noch die Sounddatei hat eine Prüfsumme; die einzige ist Oggs eigene Prüfung pro Seite, die ein gültiger Stream schon mitbringt. In die Spielinstallation wurde nichts geschrieben, im laufenden Spiel wurde nichts getestet, und der Loader kann solche Änderungen noch nicht selbst anwenden oder rückgängig machen.

  • Jeder Sound hat eine 74-Byte-Beschreibung; weil sie binär ist, passt sie nicht immer an ihren alten Platz (in einem Fall 51 zu 51 Bytes, im anderen 59 zu 60), daher muss ein vollständiger Neuaufbau als Ausweichweg bereitstehen.
  • Der Audio-Container hat keinen eigenen Index: Neue Audiodaten werden einfach ans Ende angehängt und der Zeiger des Sounds umgestellt; sonst muss nichts verschoben werden.
  • Sprachzeilen brauchen keine Sonderbehandlung; es sind normale Sound-Einträge, die in english.streamed statt in streamed.resources liegen.
  • Für den Einschlagsound, die Sprachzeile und den ffmpeg-Fall bestanden alle Prüfungen, auch absichtliche Beschädigungstests, die korrekt erkannt wurden.
  • Offen: ob das Spiel die neuen Audiodaten wirklich abspielt, was das unbestätigte Hash/ID-Feld bedeutet, und Sicherung und Rückgängigmachen der beiden Sound-Container im Loader.

Technical detailsTechnische Details

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

Round-6 research pass on replacing a sound effect and a voice line in The New Order: the full write path through the bsnf descriptor, the streamed-container offsets and sizes, the Ogg layer and its checksums. All four parts of the write path are proven by replacement round trips on copies — a sound-effect case that grows the container, a voice-line case, and a bonus case with genuinely new audio encoded with ffmpeg — with every check passing. Proven on copies only; nothing was written to the game install and nothing was tested in game.

Track tnoaudio, round 6, 2026-10-01. Builds directly on docs/audio.md (bsnf descriptor layout, §§2-3), docs/streamed.md (container internals, Ogg CRC, the 16384-byte block grid, the open "append past slot" question in its §7), wolfsdk/tnoaudio.py (the project's own shipped reader — _bsnf(), _check_ogg(), _triples() — reused unmodified throughout), wolfsdk/resources.py/wolfsdk/patch.py (the generic .index/.resources archive engine), and tools/streamed.py (the Ogg-layer reader: read_page, page_crc_ok, vorbis_ident). It also closes the one item re_probes/agent_reports/r2_wwisereplace.md explicitly left open for TNO ("the texdb-style move [...] not yet built or tested"). Read-only on both game installs and on every project file outside re_probes/tnoaudio/ and this report. Game never launched, no debugger. All mutation happened on copies under D:\wolf-decomp\tmp\tnoaudio\ and ...\tnoaudio_custom\ (deleted again after each run — confirmed empty below); nothing under <Steam library>\... or <user folder>\Saved Games was opened for writing. The real TNO install is currently patched by the maintainer's existing mods (gore, widerstand, …; base/_wolfsdk_backup/journal.json lists them) — this track reads the pristine copies in base/_wolfsdk_backup/ for the archive pairs, exactly the convention wolfsdk/tnoaudio.py's own _index_dir() already uses, and reads base/streamed.resources/base/english.streamed straight from the live install, which no applied mod touches (docs/audio.md §6: nobody writes there today).

py -3.11 re_probes/tnoaudio/roundtrip.py        # the deliverable: SFX + VO, full checks
py -3.11 re_probes/tnoaudio/custom_encode.py    # bonus: genuinely NEW audio via system ffmpeg

Question

Building on wolfsdk/tnoaudio.py, docs/audio.md, docs/streamed.md: the full write path to replace a sound effect and a voice line (bsnf descriptors, streamed containers, offsets/sizes, Ogg/other codecs, any checksums), proved as a replacement round trip on a copy, plus the smallest in-game test.

Answer

The write path has four parts, and all four are now proven, not just read.

  1. The archive-entry payload (the 74-byte bsnf descriptor) is an ordinary decl-class entry — same entry shape as entityDef/weapon, already proven patchable (memory: "TNC decl edits... decl value edits work in game"). wolfsdk.resources.Archive.rebuild() (unmodified, already shipped) rewrites it correctly. One caution found here that the docs didn't call out: the descriptor is binary, so deflate_exact's comment-padding trick (which only works on text — it refuses any payload with a \x00 in its first 512 bytes, see wolfsdk/resources.py) cannot place it in-slot on an exact-size match by luck alone. Measured directly: one test case's compressed size happened to stay 51→51 bytes, the other shifted 59→60. A real writer must always be prepared to fall back to rebuild(); it cannot assume write_in_place() will fit.

  2. The container offset/length fields live in the entry's "variable tail", 20 and 24 bytes past the fixed offset/usize/csize triple (entry.pos + 32 / entry.pos + 36, both BE u32, for the common single-language case where the tail's own count field is 1). This is not managed by wolfsdk/resources.py at all — ResourceIndex/Archive only ever touch the fixed 12-byte triple. Patching it is one struct.pack_into directly into Archive.index.buf, which is already exposed and already mutable, followed by the index's own .save() — using existing, public surface, not a new file format reader.

  3. The container itself grows by simple append, past its current end — there is no slot table to grow into (unlike TNC's IDCL/DIDX, r2_ wwisereplace.md's "in-place vs rebuild" story does not apply here: a .streamed/streamed.resources file has no index of its own at all, docs/streamed.md §2.1). Both test cases deliberately picked a donor bigger than the target, forcing this growth path rather than the easier same-size case. 16384-byte alignment was kept for the new block (matching retail's own packing convention, docs/streamed.md §2.2) but nothing in the format requires it — it is just hygiene.

  4. New content can come from two places, and both are now proven:

    • Borrowed real bytes. TNO has no Vorbis encoder anywhere in this project (same gap r2_wwisereplace.md found for TNC's Wwise Vorbis — wolfsdk is stdlib-only). The direct, no-new-tooling route is to relocate an already-shipped, already-valid Ogg Vorbis stream — copy its bytes and its whole 74-byte descriptor verbatim into the target's slot. Proven end to end for one SFX pair and one VO pair (below).
    • Genuinely new content. The system already has ffmpeg installed (a binary on PATH, not a pip package — tools/streamed.py's own prove command already shells out to it the same way). A 1.5 s tone built with nothing but the Python stdlib wave module, encoded to real Ogg Vorbis by ffmpeg -c:a libvorbis, splices into the exact same slot with the exact same mechanism and round-trips byte-for-byte through the project's own tnoaudio._check_ogg(). This means a mod author is not limited to recombining existing game sounds — a recorded or synthesized replacement is reachable too, at the cost of one external (already-present) tool call, not a new Python dependency.

Voice lines need no special handling. sound/vo/english/... entries are ordinary sample/bsnd entries — the same type, the same bsnf descriptor, the same tail layout, just addressed into english.streamed instead of streamed.resources (by path prefix, docs/audio.md/tnoaudio.py's _container()). The VO test case below used exactly the same code path as the SFX one, with no VO-specific logic anywhere. The only difference observed: VO descriptors carry the literal language name ("english") in the lang field where a non-VO sample carries an empty string — a parsing detail (see "Failed hypotheses"), not a format difference.

Checksums: there are none at the archive level. wolfsdk/resources.py's own docstring states it plainly ("There is no checksum anywhere in an entry") and docs/streamed.md §2.1 confirms the container itself has no header, table, or checksum either — TNO's \x03SER format has nothing like TNC/IDCL's header+metadata hash (idclhash.py). The only checksum anywhere in this whole path is Ogg's own per-page CRC-32, part of the Vorbis stream format itself, checked by any Vorbis decoder (and by tools/streamed.py's ogg_crc/page_crc_ok) — not by the engine's archive loader. It is real and it is strict (NEG1 below), but it only matters if you hand-corrupt a stream; relocating or re-encoding a complete, valid stream produces a file with correct CRCs automatically, nothing to compute by hand.

Evidence

Format recap (both proven by read already; restated because the write

path depends on the exact byte offsets)

Archive entry (wolfsdk/resources.py):
  ... LE-len name, LE-len path, BE u32 offset, BE u32 usize, BE u32 csize
  variable tail starts at entry.pos + 12:
    BE u32 count                 (= 1 for every sample this track touched)
    per language: char[16] name, BE u32 container-offset, BE u32 length
    -> for count=1: offset @ entry.pos+32, length @ entry.pos+36

bsnf descriptor (the entry's decompressed payload), single-language, 74 B:
  "bsnf"(4) BE u32 count=1(4) char[16] lang
  @24 BE u32 length        @28 BE u32 sub (=32, the WAVEFORMATEX sub-block's
  @32 BE u32 hash/id (unconfirmed meaning, docs/audio.md)   absolute offset)
  @36 BE u32 0              @40 BE u32 granule (PCM frame count)
  @44 BE u32 0               @48 BE u32 length (repeat of @24)
  @52 LE u16 tag(0x674F) LE u16 channels LE u32 rate LE u32 avgBytesPerSec
  @64 LE u16 blockAlign  LE u16 bits  (2 B unidentified) @70 LE u32 frames

Case SFX — imp_metal_01.wav ← imp_concrete_01.wav (growth path)

target  sound/projectile/bullet_impacts/metal/imp_metal_01.wav
        chunk0, entry.offset=3113632 csize=51 usize=74
        tail: container@0x030000 (196608) length=11586
        descriptor parses: channels=2 rate=48000 frames=20653

donor   sound/projectile/bullet_impacts/concrete/imp_concrete_01.wav
        chunk0, tail: container@0x000000 length=17619
        descriptor parses: channels=2 rate=48000 frames=38822
        LIVE container bytes verified first: 6 Ogg pages, 0 bad CRC,
        chain ends exactly at length 17619, granule 38822 == descriptor

AFTER (fresh Archive, re-read from disk, nothing held in memory):
  descriptor bytes == donor's descriptor, byte for byte         True
  tail -> container@0x034000 (212992) length=17619               (matches)
  bytes at new offset == donor's real bytes, byte for byte       True
  tnoaudio._check_ogg(bytes, 38822)  [shipped validator]         True
  independent page walk: 6 pages, 0 bad CRC, ends exactly at len True
  donor's own original region: still byte-identical (untouched)  True
  target's old bytes: still physically present at old offset,
    now simply unreferenced ("orphaned", not corrupted)          True
  NEG1 one bit flipped in the new stream -> a page CRC fails      True
  NEG2 descriptor granule off by one -> _check_ogg() = False      True
  NEG3 10 bytes truncated off the end -> _check_ogg() = False     True
  NEG4 new descriptor != target's original (a real change)        True
  CASE SFX ALL_CHECKS_OK = True

Case VO — bj_center_c01_7071.wav ← ev_...antiaircraftturretcontactcpr...wav

target  sound/vo/english/bj/bj_center_c01_7071.wav
        chunk1, tail: container@0xF8C000 (16302080) length=15937
        descriptor parses: channels=1 rate=48000 frames=46840

donor   sound/vo/english/bj/ev/ev_c01_antiaircraftturretcontactcpr_bj_center.wav
        chunk1, tail: container@0xFD0000 (16580608) length=195672
        descriptor parses: channels=1 rate=48000 frames=758352
        LIVE container bytes verified first: 47 Ogg pages, 0 bad CRC,
        chain ends exactly at length 195672, granule 758352 == descriptor

AFTER:
  descriptor rewrite: csize 59 -> 60 (NOT the same -- see Answer §1)
  tail -> container@0x1000000 (16777216) length=195672
  every check in the SFX table above: also True (identical code path)
  CASE VO ALL_CHECKS_OK = True

Bonus — genuinely new audio via the system's ffmpeg

WAV built with nothing but Python's stdlib `wave` module: 1.5 s, stereo,
48000 Hz, 16-bit, a two-tone beep (880 Hz then 1320 Hz) -- audibly not
silence, not any shipped sample.

ffmpeg -c:a libvorbis -qscale:a 4  ->  9391 B real Ogg Vorbis
  ffprobe:   codec_name=vorbis channels=2 sample_rate=48000 duration=1.5
  page walk (project's own reader): 4 pages, 0 bad CRC, ends exactly at
             length, granule=72000 (= 1.5 s * 48000 Hz, exact)

Spliced into imp_metal_01's slot, using imp_metal_01's OWN original
descriptor as the template (channels/rate/format fields untouched -- the
synthetic WAV was deliberately built stereo/48000 to match), patching only
length@24, granule@40, length-repeat@48, frames@70:

  REREAD: descriptor parses (9391, 2, 48000, 72000)
  bytes at new offset == ffmpeg's output, byte for byte           True
  tnoaudio._check_ogg()                                           True
  CUSTOM-ENCODE SPLICE ALL_CHECKS_OK = True

Scratch cleanup (rule: copies only under D:\wolf-decomp\tmp\<track>\, deleted after)

D:\wolf-decomp\tmp\tnoaudio          87 MB used during the run -> removed
D:\wolf-decomp\tmp\tnoaudio_custom    6 MB used during the run -> removed
Game install: zero bytes written (verified: both runs only ever opened
<Steam library>\...\base\_wolfsdk_backup\* and <Steam library>\...\base\
{streamed.resources,english.streamed} with mode "rb").

PROVEN

  1. The archive-entry side of a sample replacement is an ordinary decl-class entry; wolfsdk.resources.Archive.rebuild() (existing, unmodified) rewrites its 74-byte bsnf payload correctly, re-read with a fresh Archive object afterward (not trusting in-memory state).
  2. The container-offset/length fields sit in the entry's "variable tail" at entry.pos+32/entry.pos+36 (single-language case) — confirmed by direct byte patch + fresh re-read, for both streamed.resources and english.streamed.
  3. streamed.resources/english.streamed have no index, table, or checksum of their own (docs/streamed.md §2.1, re-confirmed): appending new, 16384-aligned bytes past the current end of the file and repointing one entry's tail at them is sufficient — no other file needs to move, confirmed by the regression check that the donor's own, untouched region still reads identically.
  4. The growth path (new content bigger than the old, the case r2_wwisereplace.md left untested for TNO) works for both a world SFX and a VO line, via the identical code path — voice lines require no special-casing beyond "which container": they are sample entries like any other.
  5. A genuinely new (not borrowed) Ogg Vorbis stream — built from a stdlib wave WAV and the system's ffmpeg — splices in through the same mechanism and round-trips through the project's own shipped reader.
  6. There is no checksum anywhere at the archive/container level (wolfsdk/resources.py's own docstring; docs/streamed.md); the only checksum in the whole pipeline is Ogg's per-page CRC-32, and it is genuinely sensitive to corruption (NEG1: one flipped bit fails exactly one page's CRC).
  7. wolfsdk.patch.Installation (the live apply/revert/journal machinery) has zero code path for streamed.resources/english.streamed today: ensure_backup()'s manifest only ever iterates load_master(self.base) pairs (the 14 chunkN.index/.resources), never the two streamed containers (read directly in wolfsdk/patch.py, confirmed by inspection, not merely by docs/audio.md's earlier statement of the same gap). This write path is proven structurally, but is not yet wired into the mod-apply pipeline — see "Next step".

INFERRED (not runtime-proven — no game launch, per the rules)

  1. That the engine actually plays the new content when it loads this sample id, as opposed to rejecting it for some reason not visible in the file format (e.g. an in-memory length limit, a streaming-pool size cvar like s_initStreamPoolSizeMB-equivalent for TNO if one exists — not found in this track). Structurally indistinguishable from every one of the 23,565 real streams this format already carries (same magic, same page/CRC/granule arithmetic, same descriptor shape) — but "indistinguishable on disk" is not "confirmed in a running process".
  2. That duplicate entries of the same sample name across multiple chunks (if any exist — not surveyed this pass) all share the same tail offset/length and would all need patching identically, the way wolfsdk.patch.Installation.find() already does for other asset types via its multi-chunk lookup table. Plausible by analogy, not checked for sample specifically.
  3. The hash/id field at descriptor offset 32 (docs/audio.md: "Bedeutung unbestätigt", i.e. "meaning unconfirmed") was carried over as the donor's real value (SFX/VO cases) or left as the template's value (custom-encode case) without being independently derived. Both choices are defensible (see Answer), neither is confirmed required or ignored by the engine.
  4. Whether a voiceover/voicetrack entry needs any companion edit for a replaced VO line to look right (lip-sync/viseme timing). docs/streamed.md §8 already found that voicetrack's 310-byte-per-language blocks do not point into these containers at all and are structurally decoupled from the sample audio bytes — so a replaced line's audio plays correctly regardless (proven above), but if its duration differs noticeably from the original, mouth movement in a cutscene may not match the new audio. Not a crash risk, not reached by this track (would need voicetrack's still-unparsed 310-byte block decoded first).

Failed hypotheses

  1. "VO descriptors key by the empty-string language like world SFX do." False — _bsnf() keys a VO sample's one language entry by the literal string "english", not "". Caught immediately by an assertion failure on the first VO run; fixed by reading whichever single key the parsed dict actually has, instead of assuming "". Cheap, not a dead end, but worth recording since it is exactly the kind of assumption a future writer would otherwise bake in silently.
  2. "A same-length-ish binary payload edit will usually fit write_in_place()." Not reliable — see Answer §1 (59→60 B observed). A writer must always try rebuild() as the fallback, never assume the cheap path.

Re-runnable checks

python re_probes/tnoaudio/roundtrip.py
#  CASE SFX ALL_CHECKS_OK = True
#  CASE VO  ALL_CHECKS_OK = True
#  OVERALL ALL_CHECKS_OK = True   (exit code 0)

python re_probes/tnoaudio/custom_encode.py
#  CUSTOM-ENCODE SPLICE ALL_CHECKS_OK = True   (exit code 0; needs ffmpeg on PATH)

Both scripts read only base/_wolfsdk_backup/* (pristine archive pairs) and base/{streamed.resources,english.streamed} (live, never written to by any applied mod) under the real TNO install, and write only under D:\wolf-decomp\tmp\tnoaudio*\, which they do not delete themselves — confirmed removed by hand after each run (see Evidence).

Next step / recipe

To replace one sound in a real mod, by hand, against the real install (none of this was done — no game launch, no install write, per the rules):

  1. Pick the target sample id and a donor id (or a freshly-encoded .ogg, see step 2a) the same way re_probes/tnoaudio/roundtrip.py does: Archive.find("sample", name) against base/_wolfsdk_backup/ (or base/ if unpatched), read the tail offset/length at entry.pos+32/+36, read the descriptor with archive.read(entry).
  2. Build the replacement bytes:
    • a. Borrow a real stream: read it from the live container at its own tail offset/length, and reuse its whole descriptor verbatim (this track's proven path), or
    • b. Encode something new: ffmpeg -i input.wav -c:a libvorbis out.ogg, then clone a real descriptor of the same channel count and sample rate as template and patch only length@24/granule@40/length-repeat@48/frames@70 (this track's bonus path) — channel/rate mismatches were not exercised; matching the template is the safe choice until that is tested.
  3. Before touching the real game, extend ensure_backup()-equivalent coverage by hand: copy base/streamed.resources and/or base/english.streamed to a scratch location outside the Steam folder (1.5 GB combined — fine on D:, not on the nearly-full C:) and record their current sizes. This project's own Installation._revert() already has the exact logic needed to undo a pure-append growth ("Older journals grew the file by appending; trim that back", wolfsdk/patch.py) — it just is not yet pointed at these two files. Until it is, revert is manual: truncate the container back to the recorded size and restore the one touched chunkN.index/.resources pair from base/_wolfsdk_backup/.
  4. Append the new bytes to the (real) container, 16384-aligned, patch the tail, rewrite the descriptor via Archive.rebuild(), index.save() — identical to roundtrip.py's write section, just against base/ instead of a scratch copy.
  5. Re-verify exactly as this track did: fresh Archive, re-read the descriptor, re-read the container bytes at the new offset, tnoaudio._check_ogg(), a full tools/streamed.py walk or verify run over the touched container afterward as a global regression check (every other stream must still resolve — verify already reports this as a percentage).
  6. Only then start the game.

Wiring this into wolfsdk.patch.Installation properly (not done here — outside this track's writable scope) would mean: (a) add streamed.resources/english.streamed to the backup manifest with their pristine sizes, (b) extend apply()'s payloads concept so a sample payload can carry "new audio bytes" alongside "new descriptor bytes" and the apply loop appends+patches-tail instead of only rewriting the descriptor, (c) extend revert()'s existing append-trim logic (already written for .resources rebuilds) to also cover these two files. All three are small, precedented extensions of code that already exists for a near-identical case — not a new subsystem.

Open questions

  • Does the engine enforce anything about descriptor consistency at load time (channel/rate/format matching, a maximum stream length, a streaming pool budget) that the file format alone does not show? Needs either a disassembler against the (SteamStub-encrypted) .text, or an in-game test — see below.
  • Do any sample ids appear more than once across the 14 chunks? Not surveyed this pass (INFERRED 2).
  • What exactly is the descriptor's offset-32 "hash/id" field, and is it checked? Still "Bedeutung unbestätigt" ("meaning unconfirmed") per docs/audio.md; this track did not move that forward.
  • voicetrack's 310-byte-per-language block — still unparsed (docs/streamed.md §8); only its decoupling from the audio containers was reused here, not its own content.

In-game test (for the maintainer)

(Translated from German.)

None of this was tried in the game — this track worked exclusively on copies, as required. Smallest test, in order of the lowest hurdle first:

  1. Backup first, by hand, on D: copy base/streamed.resources (1.08 GB), base/chunk0.index + base/chunk0.resources (151 MB) to D:\wolf-backup-audio\ — the existing wolfsdk backup does not back up these two containers automatically (see "Next step", point 3). Without this manual backup, a failure can only be healed via Steam's file verification (1.08 GB re-download).
  2. Rebuild the already-proven SFX case, do not decide anew: sound/projectile/bullet_impacts/metal/imp_metal_01.wav (the "metal impact" sound) gets the bytes and the descriptor of sound/projectile/bullet_impacts/concrete/imp_concrete_01.wav (the "concrete impact" sound) — exactly the steps from "Next step" 2a/4, only against base/ instead of against a copy. This pair is chosen on purpose: both sounds are easy to trigger in the game (shoot at a metal surface) and clearly distinguishable by ear — a failure is immediately audible, no guesswork needed.
  3. Start the game (always via steam://rungameid/201810/..., see README.md — started directly via the EXE, part 1 exits immediately), optionally with +s_showSounds 1 +s_drawSounds 1 (both cvars present according to docs/audio.md §5), to overlay the name of the sound actually played.
  4. Shoot at a metal surface (e.g. a door, a barrel, an industrial area). Three possible outcomes:
    • the concrete impact sound plays instead of the metal sound → the complete write path works in the real game (INFERRED 1 confirmed);
    • the normal metal sound keeps playing → the new entry was not found or was discarded (a hint at a check this track could not see);
    • crash or noise/silence → one of the unconfirmed fields (hash/ID, the two undecoded bytes at offset 68-69) is not ignorable after all.
  5. Restore: copy the copies from step 1 back over base/streamed.resources, base/chunk0.index, base/chunk0.resources, shoot at metal again, confirm that the normal sound plays again — confirms that the change really affected only this one sound.

The VO case (bj_center_c01_7071.wav ← the ambient radio line) is structurally exactly the same write path (see Evidence) and needs no test of its own to prove the mechanism — only to see when exactly this one line plays in the game, which this track did not determine ("Open questions"). For a first VO test it is simpler to take a line of which the maintainer already knows where it plays in the game, and to redirect it following the same recipe.

Related: Research Report: R2 Wwisereplace, Audio, Streamed Audio Containers, TNO Modding Surface.