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
| Property | Value |
|---|---|
| Extension(s) | .wav |
| Magic Signature | RIFF |
| Type | Streamed / Buffered Audio |
| Rust Reference | View 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.
| Variant | Discriminator | Payload starts at |
|---|---|---|
| Standard RIFF/WAVE | Begins with RIFF, and the declared RIFF size accounts for the whole file | 0x00; the file is the payload |
| SFX wrapper | No RIFF at 0x00, and RIFF present at 0x1D6 | 0x1D6 (470), where a normal RIFF/WAVE begins |
| MP3-in-WAV wrapper | Begins with RIFF, but riff_size + 8 falls short of the file length | riff_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 againstRIFF; if that fails it seeks straight to0x1D6and requiresRIFFthere, 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 separateBMU V1.0sub-format signature, then sets a flag thatCResWave::GetData(0x005df370) consumes as a plaindata + 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
0–3, plus0–7on the resident path for the unrelatedBMUcheck. Bytes8through0x1D5are never dereferenced, compared, or used as a length or checksum by either function: the skip is a realfseekor 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
RIFFat the start,RIFFat0x1D6. 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,156and313; the lengths those headers declare are156,157and157; and they total exactly 470. So the payload does not begin at an offset some tool picked, it begins where the third frame ends. Offset13holds the ASCIILAME3.93, and most of the remaining space is LAME’s0x55padding byte.The later two headers read
FF F3 62 C4, differing from the first only in the padding flag (62rather than60), which is exactly why they are 157 bytes against the first frame’s 156. Frame length for MPEG-2 Layer III is72 * bitrate / sample_rateplus the padding byte, and72 x 48000 / 22050truncates to156. 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
LAMEnorswkotor.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 on0x1D6.Where the form occurs. Every file under
streamsounds/, and some but not all ofstreammusic/. No.wavresource inside any archive takes this form, and nothing understreamwaves/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.0is a third sub-format, and this manual does not document it The resident load path checks the first eight bytes against aBMU V1.0signature before it tests forRIFFat 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-formatshandles 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 Event | Ghidra 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 8–0x1d5. 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-off | The 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. |