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.
-
The archive-entry payload (the 74-byte
bsnfdescriptor) is an ordinary decl-class entry — same entry shape asentityDef/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, sodeflate_exact's comment-padding trick (which only works on text — it refuses any payload with a\x00in its first 512 bytes, seewolfsdk/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 torebuild(); it cannot assumewrite_in_place()will fit. -
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 bywolfsdk/resources.pyat all —ResourceIndex/Archiveonly ever touch the fixed 12-byte triple. Patching it is onestruct.pack_intodirectly intoArchive.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. -
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.resourcesfile 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. -
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.mdfound for TNC's Wwise Vorbis —wolfsdkis 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
ffmpeginstalled (a binary onPATH, not a pip package —tools/streamed.py's ownprovecommand already shells out to it the same way). A 1.5 s tone built with nothing but the Python stdlibwavemodule, encoded to real Ogg Vorbis byffmpeg -c:a libvorbis, splices into the exact same slot with the exact same mechanism and round-trips byte-for-byte through the project's owntnoaudio._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.
- Borrowed real bytes. TNO has no Vorbis encoder anywhere in this
project (same gap
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
- The archive-entry side of a sample replacement is an ordinary
decl-class entry;
wolfsdk.resources.Archive.rebuild()(existing, unmodified) rewrites its 74-bytebsnfpayload correctly, re-read with a freshArchiveobject afterward (not trusting in-memory state). - 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 bothstreamed.resourcesandenglish.streamed. streamed.resources/english.streamedhave 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.- The growth path (new content bigger than the old, the case
r2_wwisereplace.mdleft 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 aresampleentries like any other. - A genuinely new (not borrowed) Ogg Vorbis stream — built from a stdlib
waveWAV and the system'sffmpeg— splices in through the same mechanism and round-trips through the project's own shipped reader. - 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). wolfsdk.patch.Installation(the live apply/revert/journal machinery) has zero code path forstreamed.resources/english.streamedtoday:ensure_backup()'s manifest only ever iteratesload_master(self.base)pairs (the 14chunkN.index/.resources), never the two streamed containers (read directly inwolfsdk/patch.py, confirmed by inspection, not merely bydocs/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)
- 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". - 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-chunklookuptable. Plausible by analogy, not checked forsamplespecifically. - 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.
- Whether a
voiceover/voicetrackentry needs any companion edit for a replaced VO line to look right (lip-sync/viseme timing).docs/streamed.md§8 already found thatvoicetrack's 310-byte-per-language blocks do not point into these containers at all and are structurally decoupled from thesampleaudio 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 needvoicetrack's still-unparsed 310-byte block decoded first).
Failed hypotheses
- "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. - "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 tryrebuild()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):
- Pick the target
sampleid and a donor id (or a freshly-encoded.ogg, see step 2a) the same wayre_probes/tnoaudio/roundtrip.pydoes:Archive.find("sample", name)againstbase/_wolfsdk_backup/(orbase/if unpatched), read the tail offset/length atentry.pos+32/+36, read the descriptor witharchive.read(entry). - 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.
- Before touching the real game, extend
ensure_backup()-equivalent coverage by hand: copybase/streamed.resourcesand/orbase/english.streamedto a scratch location outside the Steam folder (1.5 GB combined — fine onD:, not on the nearly-fullC:) and record their current sizes. This project's ownInstallation._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 touchedchunkN.index/.resourcespair frombase/_wolfsdk_backup/. - Append the new bytes to the (real) container, 16384-aligned, patch the
tail, rewrite the descriptor via
Archive.rebuild(),index.save()— identical toroundtrip.py's write section, just againstbase/instead of a scratch copy. - 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 fulltools/streamed.py walkorverifyrun over the touched container afterward as a global regression check (every other stream must still resolve —verifyalready reports this as a percentage). - 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
sampleids 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:
- Backup first, by hand, on D: copy
base/streamed.resources(1.08 GB),base/chunk0.index+base/chunk0.resources(151 MB) toD:\wolf-backup-audio\— the existingwolfsdkbackup 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). - 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 ofsound/projectile/bullet_impacts/concrete/imp_concrete_01.wav(the "concrete impact" sound) — exactly the steps from "Next step" 2a/4, only againstbase/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. - Start the game (always via
steam://rungameid/201810/..., seeREADME.md— started directly via the EXE, part 1 exits immediately), optionally with+s_showSounds 1 +s_drawSounds 1(both cvars present according todocs/audio.md§5), to overlay the name of the sound actually played. - 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.
- 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.