Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

WAV (Waveform Audio)

While standard RIFF WAV files are supported, KOTOR utilizes a multi-tiered routing structure to evaluate audio buffers dynamically based on whether the file encapsulates voice-overs (VO), ambient sound effects (SFX), or unmodified bytes.

At a Glance

PropertyValue
Extension(s).wav
Magic SignatureRIFF
TypeStreamed / Buffered Audio
Rust ReferenceView rakata_formats::Wav in Rustdocs

File Layout

There is no single layout here. A .wav in KotOR is one of three different things wearing the same extension, and which one you have is decided entirely by what the first few bytes look like.

VariantDiscriminatorPayload starts at
Standard RIFF/WAVEBegins with RIFF, and the declared RIFF size accounts for the whole file0x00; the file is the payload
SFX wrapperNo RIFF at 0x00, and RIFF present at 0x1D60x1D6 (470), where a normal RIFF/WAVE begins
MP3-in-WAV wrapperBegins with RIFF, but riff_size + 8 falls short of the file lengthriff_size + 8, where raw MP3 data begins

Telling the two wrapped forms apart

They are discriminated differently, and it is worth being precise about which is which. The SFX form is caught by the absence of RIFF at the start, not by any magic of its own. The MP3 form does start with RIFF and passes a naive check. What gives it away is arithmetic, not a signature: riff_size + 8 lands short of the file’s end, and the payload starts exactly there. A reader that trusts the RIFF tag and stops will hand the wrapper to a decoder and call the actual audio padding.

Every shipping example declares a riff_size of 50, putting the payload at byte 58, and it is tempting to treat 58 as the rule. It is not: the engine computes the offset rather than assuming it, so a wrapper of any other size resolves correctly for the engine and wrongly for a reader that hardcoded the observed value.

What the engine actually tests

Important

The engine never checks FF F3 60 C4, and its test is negative Those four bytes are not a KotOR signature. They are an ordinary MPEG frame header (FF F3 = MPEG-2 Layer III, no CRC; 60 = 48 kbps, 22.05 kHz, unpadded; C4 = mono), the first header of the MP3 stub the wrapper opens with. They are constant across shipped files only because every stub was encoded the same way.

A program-wide search for the constant, in both byte orders, returns zero hits anywhere in swkotor.exe. It is a prefix observed in shipped files, not a magic value the engine validates.

What both load paths actually do is test for the absence of something. CExoStreamingSoundSourceInternal::InitializeSource (0x005dbd30), the streaming path, reads four bytes and compares them against RIFF; if that fails it seeks straight to 0x1D6 and requires RIFF there, rejecting the file if that also fails. CResWave::OnResourceService (0x005df230), the resident path, does the same after first checking the first eight bytes against a separate BMU V1.0 sub-format signature, then sets a flag that CResWave::GetData (0x005df370) consumes as a plain data + 0x1D6.

So the discriminator is “not RIFF at the start, and RIFF at 470”. Any file meeting that shape is treated as wrapped regardless of what its first four bytes hold.

The intervening bytes are genuinely untouched. Across both paths the only bytes read inside the 470-byte span are 03, plus 07 on the resident path for the unrelated BMU check. Bytes 8 through 0x1D5 are never dereferenced, compared, or used as a length or checksum by either function: the skip is a real fseek or pointer add, not a disguised read.

Keying on the four-byte prefix instead is stricter than the engine and refuses wrapped files it accepts. Rakata’s reader applies the engine’s own test: no RIFF at the start, RIFF at 0x1D6. Its writer still emits the prefix, deliberately, because every file that takes this form carries exactly the same block and other tools do key on it even though the game does not.

The 470-byte prefix

Note

The 470-byte prefix is three MP3 frames, which is why it is 470 bytes The engine skips the block, but a writer still has to produce it, so it is worth saying what it is rather than leaving it as an opaque run. It is fixed: byte for byte identical in every wrapped file in a retail install, with no variation at all.

It decodes as three MPEG-2 Layer III frames at 48 kbps and 22.05 kHz. Frame headers sit at 0, 156 and 313; the lengths those headers declare are 156, 157 and 157; and they total exactly 470. So the payload does not begin at an offset some tool picked, it begins where the third frame ends. Offset 13 holds the ASCII LAME3.93, and most of the remaining space is LAME’s 0x55 padding byte.

The later two headers read FF F3 62 C4, differing from the first only in the padding flag (62 rather than 60), which is exactly why they are 157 bytes against the first frame’s 156. Frame length for MPEG-2 Layer III is 72 * bitrate / sample_rate plus the padding byte, and 72 x 48000 / 22050 truncates to 156. That arithmetic is the whole of why the prefix is 470.

The wrapper is therefore an MP3 stub with a RIFF/WAVE file glued on behind it. What it was for is not established, and one specific explanation has been checked and did not hold: neither LAME nor Xing appears anywhere in swkotor.exe, so the stub is not boilerplate the game itself emits or recognises by name. That does not rule out an encoder having produced it upstream in BioWare’s tooling; it rules out the game being the thing that put it there. What it means for a writer is that the block gets reproduced verbatim, and that the frame arithmetic is an independent check that a copy is intact: decode the three headers and the lengths have to land on 0x1D6.

Where the form occurs. Every file under streamsounds/, and some but not all of streammusic/. No .wav resource inside any archive takes this form, and nothing under streamwaves/ does either, since those are all ordinary RIFF/WAVE. So “SFX” here is a shape a few directories use, not a property of the extension.

BMU V1.0, and where the Rust implementation stops

Note

BMU V1.0 is a third sub-format, and this manual does not document it The resident load path checks the first eight bytes against a BMU V1.0 signature before it tests for RIFF at all. Nothing else in these pages mentions it and Rakata does not handle it. It is recorded here because a reader tracing the engine’s audio dispatch will meet the branch and find nothing about it anywhere else.

Note

Scope of the Rust implementation rakata-formats handles the container tier only: identifying which of the three wrappers a file uses, and getting to the payload inside it. Sample decoding is not its job, which mirrors the engine, where the executable routes to Miles Sound System and performs almost no chunk parsing of its own.

Engine Audits & Decompilation

Read from CExoSoundInternal::LoadSoundProvider at 0x005d9140 in swkotor.exe. Provenance: derived, not attested unless a claim says otherwise: the rows have not been separately re-derived, so they sit on the reverse-engineering queue. Individual claims below may carry a level of their own, and where one does it overrides this line for that claim.

Pipeline EventGhidra Provenance & Engine Behavior
Standard Audio (WAV)If the payload begins with the exact "RIFF" 4-byte signature and evaluates dynamically as a non-MP3 track, the parser initiates at offset 0 and transmits the contiguous buffer to the Miles Sound System without execution modification.
Ambient Audio (SFX)Reached by failing the "RIFF" test at offset 0, not by matching anything. The engine then requires "RIFF" at +0x01d6 and rejects the file if it is absent. It does not interpret the 470-byte prefix in any way: it seeks or pointer-adds past it, reading none of bytes 80x1d5. The payload is the remainder, size = file_size - 0x1d6.
Voice Audio (VO)For streaming voice-over tracks, the .wav wrapper successfully begins with a "RIFF" tag. However, structural logic asserting riff_size + 8 < file_size effectively succeeds. The memory engine immediately seeks to byte offset riff_size + 8 and subsequently pipes the remaining data exclusively as a literal .mp3 stream.
Delegation Hand-offThe main executable natively acts as a dispatch router, executing almost zero internal chunk structural parsing routines. Total specialization for deep RIFF chunk deserialization is deferred unconditionally to the external Miles Sound System layer.