Audio and Localization Formats - OpenKotOR/PyKotor GitHub Wiki
The KotOR engine handles text, voice, and character sound effects through a set of interconnected formats. TLK (Talk Table) files store every localized string the game displays or speaks — dialogue lines, item names, journal entries, feedback messages [tlk_data.py]. SSF (Sound Set File) files map character combat and movement sounds to TLK entries. LIP files drive facial animation timing to match spoken dialogue. WAV files provide the raw audio data. Together, these formats implement the localization and voice-over pipeline that runs from authored content through to in-game playback. xoreos-tools mirrors that workflow from the tooling side with dedicated TLK conversion utilities and an explicit language-ID/encoding matrix for Aurora/Odyssey talk tables, which makes it a useful external corroboration source for KotOR localization behavior [Running xoreos-tools, Tlk2xml, TLK language IDs and encodings].
The Talk Table is the game's central string database. Every piece of text the player sees — dialogue, item descriptions, journal entries, feedback messages, character names — is stored in dialog.tlk and accessed by a numeric index called a StrRef [tlk_data.py L1–L35]. This design makes localization straightforward: translating the game means replacing one file rather than hunting through thousands of individual resources. Each entry can also reference a voice-over sound file (ResRef), so the engine can play spoken audio alongside the displayed text [TLKEntry]. xoreos's tlk2xml page is also useful here because it describes why TLK tooling cannot rely on the header alone to infer text encoding: the format family reuses language IDs differently across games, and tools often need either an explicit game target or an explicit code page [Tlk2xml, TLK language IDs and encodings].
To modify TLK entries in a mod, use TSLPatcher/HoloPatcher TLKList syntax — this appends new entries or patches existing ones without replacing the entire file, which is critical for mod compatibility. TLK entries are referenced from GFF resources (especially DLG dialogue files), 2DA tables, and SSF sound sets.
- TLK — Talk Table
TLK files store localized strings in a binary format. The game loads dialog.tlk at startup and references strings throughout the game using StrRef numbers (array indices). String lookups use the same resource resolution order as other resources (override, then module/SAV, then KEY/BIF), so custom TLKs in override or in a MOD take precedence over the base game dialog.tlk. External tooling reflects the same constraints: xoreos's tlk2xml can read the classic V3.0/V4.0 talk-table families and exposes game-specific switches such as --kotor and --kotor2 precisely because the encoding cannot be safely autodetected from TLK content alone [Tlk2xml].
Implementation: Libraries/PyKotor/src/pykotor/resource/formats/tlk/
Cross-reference implementations (line anchors are against master and may drift):
-
PyKotor:
- binary layout in module docstring:
tlk_data.pyL1–L39 -
TLK/TLKEntry:tlk_data.pyL54–L282 tlk_data.pyL285–L420- read path (Kaitai + legacy 40-byte rows):
io_tlk.py_load_tlk_from_kaitaiL32–L63 _load_tlk_legacyL66–L120TLKBinaryReader.loadL149–L156- write path:
TLKBinaryWriterL168–L219 (_write_file_headerL183–L192,_write_entryL194–L219)
- binary layout in module docstring:
-
-
tlkreader.cppTlkReader::loadL34–L43 (8-byte"TLK V3.0"signature + language/count/offset) -
loadStringsL45–L71 (40-byte rows, lowercases sound ResRef) - flag constants L28–L32
-
-
xoreos —
talktable.cpp(Aurora talk table shared with other Aurora titles) -
xoreos-tools —
talktable.cpp(CLI / tooling) -
KotOR.js:
TLKObject.tsLoadFromBufferL51–L96 — 20-byte header then per-entry metadata; note the last field before text is read withreadUInt32()while the on-disk field is a float sound length—compare PyKotorstruct.unpack("<f", ...)L106. -
Kotor.NET:
TLKBinaryStructure.csFileHeader/StringData/FileRootL16–L130.
- 2DA File Format - Game tables with name/description StrRefs
- GFF File Format - Dialogue and templates that reference TLK strings
- SSF File Format - Sound sets that reference TLK entries
- TSLPatcher TLKList Syntax - Modding TLK files with TSLPatcher
The file header is 20 bytes in size:
| Name | type | offset | size | Description |
|---|---|---|---|---|
| file type | char | 0 (0x00) | 4 | Always "TLK " (space-padded) |
| file Version | char | 4 (0x04) | 4 |
"V3.0" for KotOR, "V4.0" for Jade Empire |
| Language ID | int32 | 8 (0x08) | 4 | Language identifier (see Concepts; TLK-specific notes) |
| string count | int32 | 12 (0x0C) | 4 | Number of string entries in the file |
| string Entries offset | int32 | 16 (0x10) | 4 | offset to string entries array (typically 20) |
References:
- reone
TlkReader::loadL34–L43 - PyKotor header parse —
_load_tlk_legacyL71–L75 - PyKotor Kaitai path —
_load_tlk_from_kaitaiL35–L44 - Kotor.NET
FileHeaderL66–L76 -
KotOR.js
LoadFromBufferL59–L64.
The string data table contains metadata for each string entry. Each entry is 40 bytes:
| Name | type | offset | size | Description |
|---|---|---|---|---|
| flags | UInt32 | 0 (0x00) | 4 | bit flags: bit 0=text present, bit 1=sound present, bit 2=sound length present |
| Sound ResRef | char | 4 (0x04) | 16 | Voice-over audio filename (null-terminated, max 16 chars) |
| Volume Variance | UInt32 | 20 (0x14) | 4 | Unused in KotOR (always 0) [tlk_data.py L27, io_tlk.py L214] |
| Pitch Variance | UInt32 | 24 (0x18) | 4 | Unused in KotOR (always 0) [tlk_data.py L28, io_tlk.py L215] |
| offset to string | UInt32 | 28 (0x1C) | 4 | offset to string text (relative to string Entries offset) |
| string size | UInt32 | 32 (0x20) | 4 | Length of string text in bytes |
| Sound Length | float | 36 (0x24) | 4 | Duration of voice-over audio in seconds |
References:
- Kotor.NET
StringDataL92–L129 - PyKotor row decode
_load_tlk_legacyL96–L112 - reone
loadStringsL45–L69 -
KotOR.js
LoadFromBufferL65–L75.
flag bits:
- bit 0 (0x0001): Text present - string has text content
- bit 1 (0x0002): Sound present - string has associated voice-over audio
- bit 2 (0x0004): Sound length present - sound length field is valid
flag Combinations:
Common flag patterns in KotOR TLK files:
| flags | Hex | Description | Usage |
|---|---|---|---|
0x0001 |
0x01 |
Text only | Menu options, item descriptions, non-voiced dialog |
0x0003 |
0x03 |
Text + Sound | Voiced dialog lines (most common for party/NPC speech) |
0x0007 |
0x07 |
Text + Sound + Length | Fully voiced with duration data (cutscenes, important dialog) |
0x0000 |
0x00 |
Empty entry | Unused StrRef slots |
The engine uses these flags to decide:
- Whether to display subtitles (Text present flag) [
io_tlk.pyL108] - Whether to play voice-over audio (Sound present flag) [
io_tlk.pyL109]
The soundlength-present flag (bit 2) is parsed into soundlength_present [io_tlk.py L110] but is noted as "Unused by KOTOR1 and 2" in PyKotor's writer [io_tlk.py L210]. The sound_length float stores duration in seconds but the engine does not appear to require the flag to be set to read that value.
Missing flags are treated as false by PyKotor's reader [io_tlk.py L108–L110] — if Text present is not set, the string is treated as empty even if text data exists.
String entries follow the string data table:
| Name | Type | Description |
|---|---|---|
| string Text | char[] | null-terminated string data (UTF-8 or Windows-1252 encoded) |
string text is stored at the offset specified in the string data table entry. The encoding depends on the language ID; see:
Each TLK entry contains:
References:
- PyKotor
TLK/TLKEntry—tlk_data.pyL54–L420 - entry serialization —
io_tlk.py_write_entryL194–L219.
| Attribute | Type | Description |
|---|---|---|
text |
str | Localized text string |
voiceover |
ResRef | Voice-over audio filename (WAV file) |
text_present |
bool | Whether text content exists |
sound_present |
bool | Whether voice-over audio exists |
soundlength_present |
bool | Whether sound length is valid |
sound_length |
float | Duration of voice-over audio in seconds |
string references (StrRef) are integer indices into the TLK file's entry array:
- StrRef 0: First entry in the TLK file
- StrRef -1: No string reference (used to indicate missing/empty strings)
- StrRef N: Nth entry (0-based indexing)
The game uses StrRef values throughout GFF files, scripts, and other resources to reference localized text. When displaying text, the game looks up the StrRef in dialog.tlk and displays the corresponding text.
Mods can add custom TLK files to extend available strings:
dialog.tlk structure:
- Base game:
dialog.tlk(read-only, ~50,000-100,000 entries) - Custom content:
dialogf.tlkor custom TLK files placed in override
StrRef Ranges:
-
0to~50,000: Base game strings (varies by language) -
16,777,216(0x01000000) and above: Custom TLK range (TSLPatcher convention) - Negative values: Invalid/missing references
Mod Tools Approach:
TSLPatcher and similar tools use high StrRef ranges for custom strings:
Base [StrRef](Audio-and-Localization-Formats#string-references-strref): 0 - 50,000 (dialog.tlk)
Custom Range: 16777216+ (custom TLK files)
This avoids conflicts with base game strings and allows mods to add thousands of custom text entries without overwriting existing content.
Multiple TLK files:
The game can load multiple TLK files:
-
dialog.tlk- Primary game text -
dialogf.tlk- Female-specific variants (polish K1 only)
Priority: Custom TLKs --> dialogf.tlk --> dialog.tlk
Numeric language IDs and typical encodings (0–8) are defined on Concepts. The notes below focus on TLK-specific behavior.
KotOR: Retail builds usually install one dialog.tlk per language; the file header’s Language ID is often ignored at runtime and is mainly useful for tools tagging which language a TLK was built for.
Encoding: Western languages (IDs 0–4) normally use Windows-1252 in legacy tooling; Polish (5) uses Windows-1250. IDs 6–8 use UTF-8 in the wider Aurora spec. String payloads may still be UTF-8 or Windows-1252 depending on the tool and target game—see the string entry description above.
Windows-1252 is a single-byte encoding (256 code points) and is often loosely called "ISO-8859-1" or cp1252.
| Layer | PyKotor (master) |
|---|---|
| Layout spec (docstring) | tlk_data.py L1–L39 |
TLK / TLKEntry
|
tlk_data.py L54–L420 |
| Binary read |
io_tlk.py L32–L156 (TLKBinaryReader L123–L156) |
| Binary write | io_tlk.py L159–L219 |
Kotor.NET: TLKBinaryStructure.cs L16–L130.
See Cross-reference implementations under File structure overview for reone, xoreos, KotOR.js, and KotOR-Unity.
- Concepts -- Language IDs and encodings
- TSLPatcher TLKList Syntax -- Modifying TLK via HoloPatcher/TSLPatcher
- 2DA-File-Format
- GFF-File-Format -- StrRef consumers
- NSS-File-Format -- Script strings
- Bioware-Aurora-TalkTable -- Aurora talk table spec
- Concepts -- Resource resolution
- Container-Formats#key -- KEY/BIF index
- Community sources and archives -- DeadlyStream, forums for TLK/StrRef modding
SSF files map 28 creature sound events — battle cries, selection acknowledgements, pain grunts, death screams — to StrRef indices into dialog.tlk. Each creature template references an SSF by ResRef, and the engine plays the associated voice-over line whenever the corresponding event fires. Slots that store -1 (0xFFFFFFFF) produce no sound.
To modify SSF files in mods, see the TSLPatcher SSFList syntax guide. SSF StrRefs resolve through the same TLK string table used for dialogue and UI text.
- SSF — Sound Set File
SSF files define 28 logical sound slots (indices 0–27) that creatures use for battle cries, selection lines, grunts, UI feedback, etc. Each slot holds a StrRef into dialog.tlk (or -1 / 0xFFFFFFFF for “no sound”). SSF files load through the same resource resolution order as other resources (override, MOD/SAV, KEY/BIF).
On-disk size:
- The header is 12 bytes; the semantic KotOR table is 28 × 4 = 112 bytes.
- Some writers emit 40 × 4 = 160 bytes after the header (28 mapped slots plus 12 extra
0xFFFFFFFFwords). - PyKotor’s writer does this (
io_ssf.pyL177–L181). - Kotor.NET models a 40-entry table in
SSFBinaryStructure.csSoundTableL61–L77. - Readers that only consume the first 28 entries after
offsetstill match vanilla behavior. - KotOR.js
SSFObject.OpenderivessoundCount = (length - 12) / 4and therefore accepts either width.
Implementation (PyKotor): Libraries/PyKotor/src/pykotor/resource/formats/ssf/
Cross-reference implementations (line anchors are against master and may drift):
-
PyKotor:
- format notes in module docstring:
io_ssf.pyL1–L42 - legacy load path (28 slots, order fixed):
_load_ssf_legacyL63–L112 - reader dispatch:
SSFBinaryReader.loadL152–L159 - writer:
SSFBinaryWriter.writeL171–L181 - enum + semantics:
SSFSoundL123–L234
- format notes in module docstring:
-
reone —
ssfreader.cppSsfReader::loadL28–L36 (validatesSSF V1.1, seeks to table offset, reads all remainingint32s into an array — works for 28- or 40-word tables). -
xoreos — Aurora SSF (
src/aurora/ssffile.cpp), shared with other Aurora titles. -
SSFObject.ts- slot names:
SSFTypeenum L14–L43 (same indices as PyKotorSSFSound; identifier spellings differ slightly — see table below)
-
- header + 40-slot table:
SSFBinaryStructure.csL29–L77 - high-level read loop (28 creature sounds):
SSFBinaryReader.ReadL31–L45
- header + 40-slot table:
- TSLPatcher SSFList Syntax - Modding SSF files with TSLPatcher
- TLK File Format - Talk Table containing actual sound references
- Bioware Aurora SSF Format - Official BioWare specification
- GFF-UTC - creature templates that reference SSF files
- 2DA-soundset - Sound set definitions table
The file header is 12 bytes in size:
| Name | type | offset | size | Description |
|---|---|---|---|---|
| file type | char | 0 (0x00) | 4 | Always "SSF " (space-padded) |
| file Version | char | 4 (0x04) | 4 | Always "V1.1"
|
| offset to Sound Table | UInt32 | 8 (0x08) | 4 | Byte offset to the first StrRef (almost always 12) |
After the header, the file contains a contiguous array of little-endian int32 StrRefs. KotOR uses the first 28 entries (indices 0–27) as in the Sound event types table below. -1 or 0xFFFFFFFF means “no sound” for that slot.
Some files and tools use 40 uint32 entries (extra trailing -1 words). Treat anything beyond index 27 as padding unless you have a specific toolchain that assigns meaning to it.
Indices are fixed; do not reorder. PyKotor names are authoritative for this repo; KotOR.js SSFType uses the same numeric values with different spellings on a few rows (noted in the third column).
| Index | PyKotor SSFSound
|
KotOR.js SSFType (if different) |
Role |
|---|---|---|---|
| 0–5 |
BATTLE_CRY_1 … BATTLE_CRY_6
|
(same) | Combat entry / battle cries |
| 6–8 |
SELECT_1 … SELECT_3
|
(same) | Creature selected |
| 9–11 |
ATTACK_GRUNT_1 … ATTACK_GRUNT_3
|
ATTACK_1 … ATTACK_3
|
Attack animation grunts |
| 12–13 |
PAIN_GRUNT_1 … PAIN_GRUNT_2
|
PAIN_1 … PAIN_2
|
Damage reactions |
| 14 | LOW_HEALTH |
(same) | Low HP warning |
| 15 | DEAD |
(same) | Death |
| 16 | CRITICAL_HIT |
(same) | Critical hit feedback |
| 17 | TARGET_IMMUNE |
(same) | Immune target |
| 18 | LAY_MINE |
(same) | Place mine |
| 19 | DISARM_MINE |
(same) | Disarm mine |
| 20 | BEGIN_STEALTH |
STEALTH |
Enter stealth |
| 21 | BEGIN_SEARCH |
SEARCH |
Search mode |
| 22 | BEGIN_UNLOCK |
UNLOCK |
Start unlock |
| 23 | UNLOCK_FAILED |
UNLOCK_FAIL |
Unlock failed |
| 24 | UNLOCK_SUCCESS |
(same) | Unlock succeeded |
| 25 | SEPARATED_FROM_PARTY |
SOLO_MODE |
Left party / solo |
| 26 | REJOINED_PARTY |
PARTY_MODE |
Rejoined party |
| 27 | POISONED |
(same) | Poisoned |
Primary references:
| Component | Location |
|---|---|
SSF data model (SSF) |
ssf_data.py SSF L26–L121 |
SSF data model (SSFSound) |
SSFSound L123–L234 |
| Binary I/O |
io_ssf.py (see File structure overview for line-level anchors) |
LIP files drive mouth animation for voiced dialogue. Each file stores a compact series of keyframes that pair a timestamp (in seconds) with a viseme index (0–15), and the engine interpolates between them while playing the companion WAV voice line. The LIP length field must match the WAV playback duration exactly, or the mouth animation will desynchronize. LIP files load through the same resource resolution order as other game assets.
LIP is always paired with a WAV of matching ResRef. Dialogue nodes in DLG trigger the voice line, which in turn triggers the LIP animation on the speaking creature model.
- LIP — Lip Synchronization
-
LIP files are always binary (
"LIP V1.0"signature) and contain only animation data. -
They are paired with WAV voice-over resources of identical duration; the LIP
lengthfield must match the WAVdataplayback time for glitch-free animation. -
keyframes are sorted chronologically and store a timestamp (float seconds) plus a 1-byte viseme index (0–15).
-
The layout is identical across these implementations (header / keyframe offsets below are cross-confirmed):
Implementation (PyKotor):
- package:
resource/formats/lip/ - binary read
LIPBinaryReader.loadL95+ - write
LIPBinaryWriter.writeL117+ - data model in
lip_data.py - XML/JSON in
io_lip_xml,io_lip_json
Comparable LIP implementations include reone's C++ parser (lipreader.cpp L27+), xoreos's Aurora reader (lipfile.cpp), KotOR.js's LIP object and binary reader (LIPObject.ts L26+, readBinary L112-L136), and Kotor.NET's in-memory DTO layer (LIP.cs L11+).
- TLK File Format - Talk Table containing voice-over references
- WAV File Format - Audio format paired with LIP files
- GFF-DLG - dialogue files that trigger LIP animations
| Name | type | offset | size | Description |
|---|---|---|---|---|
| file type | char | 0 (0x00) | 4 | Always "LIP "
|
| file Version | char | 4 (0x04) | 4 | Always "V1.0"
|
| Sound Length | float32 | 8 (0x08) | 4 | Duration in seconds (must equal WAV length) |
| Entry count | UInt32 | 12 (0x0C) | 4 | Number of keyframes immediately following |
The LIP header layout is corroborated by reone's parser (lipreader.cpp L27-L42).
keyframe Table
keyframes follow immediately after the header; there is no padding.
| Name | type | Offset (per entry) | size | Description |
|---|---|---|---|---|
| Timestamp | float32 | 0 (0x00) | 4 | Seconds from animation start |
| Shape | uint8 | 4 (0x04) | 1 | Viseme index (0–15) |
-
Entries are stored sequentially and must be sorted ascending by timestamp.
-
Libraries average multiple implementations to validate this layout:
KotOR.js's binary reader shows the same contiguous keyframe-table decode (LIPObject.ts readBinary L112-L136).
KotOR reuses the 16-shape Preston Blair phoneme set. Every implementation agrees on the byte value assignments; KotOR.js only renames a few labels but the indices match.
| Value | Shape | Description |
|---|---|---|
| 0 | NEUTRAL | Rest/closed mouth |
| 1 | EE | Teeth apart, wide smile (long "ee") |
| 2 | EH | Relaxed mouth ("eh") |
| 3 | AH | Mouth open ("ah/aa") |
| 4 | OH | Rounded lips ("oh") |
| 5 | OOH | Pursed lips ("oo", "w") |
| 6 | Y | Slight smile ("y") |
| 7 | STS | Teeth touching ("s", "z", "ts") |
| 8 | FV | Lower LIP touches teeth ("f", "v") |
| 9 | NG | Tongue raised ("n", "ng") |
| 10 | TH | Tongue between teeth ("th") |
| 11 | MPB | Lips closed ("m", "p", "b") |
| 12 | TD | Tongue up ("t", "d") |
| 13 | SH | Rounded relaxed ("sh", "ch", "j") |
| 14 | L | Tongue forward ("l", "r") |
| 15 | KG | Back of tongue raised ("k", "g", "h") |
PyKotor's LIPShape table and related phoneme helpers define the same core viseme set used throughout the surrounding LIP tooling ecosystem.
animation Rules
-
Interpolation: The engine interpolates between consecutive keyframes, and PyKotor's
LIP.get_shapes()computes the left and right visemes plus blend factor from that timeline. - Sorting: When adding frames, PyKotor removes existing entries at the same timestamp and keeps the list sorted.
-
Duration Alignment: PyKotor updates
LIP.lengthto the maximum timestamp so exported animations stay aligned with their WAV line. -
Generation: Automated pipelines (MDLOps, KotORBlender) map phonemes to visemes via
LIPShape.from_phoneme(), and the same mapping table appears in the vendor projects referenced above to keep authoring tools consistent.
PyKotor's LIP reader and data model (io_lip.py, lip_data.py) stay aligned with the same header and keyframe encoding used by reone (lipreader.cpp L27+), xoreos (lipfile.cpp), KotOR.js (LIPObject.ts L26+), and Kotor.NET (LIP.cs L11+).
KotOR stores both standard WAV voice-over lines and Bioware-obfuscated sound-effect files. Voice-over assets are regular RIFF containers with PCM headers, while SFX assets prepend a 470-byte custom block before the RIFF data. PyKotor handles both variants transparently. WAV files are resolved using the same resource resolution order as other resources:
Hex type id 0x0004 is listed under Resource type identifiers.
Implementation (PyKotor)
-
Binary reader/writer:
io_wav.pyWAVBinaryReaderL100+-
loadL148+ (SFX obfuscation + RIFF) WAVBinaryWriterL302+- RIFF parse helper
_parse_riff_wave_from_kaitaiL56+
-
Data model:
wav_data.py.
Cross-reference implementations (line anchors are against master and may drift):
-
reone —
wavreader.cpp—WavReader::loadL32+ (470-byte SFX signatureFF F3 60 C4then seek0x1DA, RIFF/fmt/data, MP3-in-WAV edge case). -
KotOR.js —
AudioFile.ts— SFX probefakeHeaderTest/riffHeaderTestL12-L13, strip 470-byte prefixprocessFileL118-L124, MP3-in-WAVriffSize == 50branch L131-L137. -
Kotor.NET — no dedicated
KotorWAVreader on the default branch; creature sound StrRefs (resolved to.wavvia TLK at runtime) are modeled inKotor.NET/Formats/KotorSSF/SSF.csL12+. For byte-level RIFF/SFX layout, prefer the implementations above:
For mod developers: WAV files are referenced from:
See HoloPatcher README for Mod Developers.
Related formats:
- WAV — Audio
| Type | Usage | Description |
|---|---|---|
| VO (Voice-over) | Dialogue lines (*.wav referenced by TLK StrRefs). |
Plain RIFF/WAVE PCM files readable by any media player. |
| SFX (Sound effects) | Combat, UI, ambience, .wav files under StreamSounds/SFX. |
Contains a Bioware 470-byte obfuscation header followed by the same RIFF data. |
PyKotor exposes these via the WAVType enum (VO vs. SFX) so tools know whether to insert/remove the proprietary header (io_wav.py:52-121).
KotOR sticks to the canonical RIFF chunk order:
| Offset | Field | Description |
|---|---|---|
| 0 (0x00) | "RIFF" |
Chunk ID |
| 4 (0x04) | <uint32> |
file size minus 8 |
| 8 (0x08) | "WAVE" |
format tag |
| 12 (0x0C) | "fmt " |
format chunk ID |
| 16 (0x10) | <uint32> |
format chunk size (usually 0x10) |
| … | See below |
| Field | Type | Description |
|---|---|---|
audio_format |
uint16 |
0x0001 for PCM, 0x0011 for IMA ADPCM. |
channels |
uint16 | 1 (mono) or 2 (stereo). |
sample_rate |
uint32 | Typically 22050 Hz (SFX) or 44100 Hz (VO). |
bytes_per_sec |
uint32 |
sample_rate × block_align. |
block_align |
uint16 | Bytes per sample frame. |
bits_per_sample |
uint16 | 8 or 16 for PCM. |
extra_bytes |
… | Present only when fmt_size > 0x10 (e.g., ADPCM coefficients). |
After the fmt chunk (and any optional fact chunk), the "data" chunk begins:
| Field | Description |
|---|---|
"data" |
Chunk ID. |
<uint32> |
Number of bytes of raw audio. |
<byte[]> |
PCM/ADPCM sample data. |
KotOR voice-over WAVs add a "fact" chunk with a 32-bit sample count, which PyKotor writes for compatibility (io_wav.py:182-186).
- SFX assets start with 470 bytes of obfuscated metadata (magic numbers plus filler
0x55). - After this header, the file resumes at the
"RIFF"signature described above. - When exporting SFX, PyKotor recreates the header verbatim so the game recognizes the asset (
io_wav.py:150-163).
(See Cross-reference implementations above for reone / KotOR.js / Kotor.NET line anchors.)
-
PCM (
audio_format = 0x0001): Most dialogue is 16-bit mono PCM, which streams directly through the engine mixer. -
IMA ADPCM (
audio_format = 0x0011): Some ambient SFX use compressed ADPCM frames; when present, thefmtchunk includes the extra coefficient block defined by the WAV spec. - KotOR requires
block_alignandbytes_per_secto match the values implied by the codec; mismatched headers can crash the in-engine decoder.
External tooling such as SithCodec and SWKotOR-Audio-Encoder implement the same formats; PyKotor simply exposes the metadata so conversions stay lossless.
- Deadly Stream — SithCodec (Windows tool for KotOR audio header strip/add; pair with PyKotor
io_wavwhen verifying bytes). - Deadly Stream — Extracting dialogue — community-reported layout notes (e.g. some assets under
StreamWaves/StreamVoice); treat thread as workflow hints, not normative RIFF layout—verify against PyKotor / reone / KotOR.js above.
TSLPatcher-era threads discuss Streamsounds / Streamwaves paths, codec issues in Miles-based workflows, and DLG-related StreamVoice layout (e.g. alienvo.2da -> StreamVoice\AVO\). Treat these as dated player/modder reports, not engine specifications—pair with SithCodec, PyKotor io_wav, and the cross-refs at the top of this page.
- Convert KOTOR sounds to a usable audio file
- Editing KotOR voiceover files — which program?
- Listening and editing KotOR I & II audio
- KotOR/TSL GUI Dialog Editor (DLGEditor) —
StreamVoice/alienvodiscussion (e.g. p.2)
-
Reference implementations (engines): same as Cross-reference implementations at the top of this page (PyKotor
io_wav.py, reonewavreader.cpp, KotOR.jsAudioFile.ts). - Community tooling (not normative): SithCodec — encode/decode helper; SWKotOR-Audio-Encoder — batch-friendly encoder. Prefer verifying headers against PyKotor / reone / KotOR.js when debugging engine mismatches.
With this structure, WAV assets authored in PyKotor will play identically in the base game and in the other vendor tools.
-
Resource formats and resolution - Resource type identifiers (
WAV/0x0004) - TLK File Format - Talk table that references WAV voice-over
- SSF File Format - Sound set files that reference WAV via StrRef
- LIP File Format - Lip-sync paired with WAV dialogue
- GFF-DLG - Dialogue files that reference WAV (VO_ResRef)