Skip to main content

rakata_formats/wav/
mod.rs

1//! WAV reader and writer with KotOR obfuscation handling.
2//!
3//! KotOR uses multiple wrappers around audio payloads:
4//! - standard RIFF/WAVE files,
5//! - SFX files with a 470-byte prefix header,
6//! - MP3-in-WAV wrappers (58-byte prefix followed by raw MP3 data).
7//!
8//! This module implements container-level parsing/serialization only. It does
9//! not decode PCM/ADPCM sample streams.
10//!
11//! ## Telling the three apart
12//!
13//! A `.wav` here is one of three things wearing the same extension, and the
14//! discriminators are not alike. An SFX file is caught by magic: it has no
15//! `RIFF` at the start, just the fixed prefix `FF F3 60 C4`, with the real
16//! payload 470 bytes in.
17//!
18//! The MP3 wrapper is the one to be careful with, because it *does* start with
19//! `RIFF` and passes a naive signature check. What exposes it is arithmetic:
20//! its header declares a RIFF size of 50, so `riff_size + 8` lands at byte 58
21//! with the rest of the file beyond it. Trusting the tag and stopping there
22//! yields 58 bytes of wrapper and treats the actual audio as padding.
23//!
24//! Byte-level offsets for all three live in `docs/src/formats/audio/wav.md`,
25//! with the engine's own dispatch sequence.
26
27pub mod adpcm;
28pub mod pcm;
29mod reader;
30mod writer;
31
32pub use reader::{read_wav, read_wav_from_bytes};
33pub use writer::{
34    write_wav, write_wav_to_vec, write_wav_to_vec_with_options, write_wav_with_options,
35};
36
37use num_enum::{IntoPrimitive, TryFromPrimitive};
38use thiserror::Error;
39
40use crate::binary::{self, DecodeBinary, EncodeBinary};
41
42pub(super) const RIFF_MAGIC: [u8; 4] = *b"RIFF";
43pub(super) const WAVE_MAGIC: [u8; 4] = *b"WAVE";
44pub(super) const FMT_CHUNK_ID: [u8; 4] = *b"fmt ";
45pub(super) const DATA_CHUNK_ID: [u8; 4] = *b"data";
46/// The four bytes every retail SFX wrapper opens with.
47///
48/// Write-side only. The engine never tests for this constant -- it appears
49/// nowhere in the executable -- and detects a wrapped file positionally
50/// instead, so the reader must not gate on it. See
51/// `docs/src/formats/audio/wav.md`.
52pub(super) const SFX_MAGIC: [u8; 4] = [0xFF, 0xF3, 0x60, 0xC4];
53pub(super) const SFX_HEADER_SIZE: usize = 470;
54pub(super) const VO_HEADER_SIZE: usize = 20;
55pub(super) const MP3_IN_WAV_RIFF_SIZE: u32 = 50;
56pub(super) const MP3_IN_WAV_HEADER_SIZE: usize = 58;
57pub(super) const DEFAULT_MP3_CHANNELS: u16 = 2;
58pub(super) const DEFAULT_MP3_SAMPLE_RATE: u32 = 44_100;
59pub(super) const DEFAULT_MP3_BITS_PER_SAMPLE: u16 = 16;
60
61/// WAV wrapper variant detected in source bytes.
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
63pub enum WavWrapperKind {
64    /// Standard RIFF/WAVE payload with no KotOR prefix.
65    Standard,
66    /// KotOR SFX-prefixed wrapper (470-byte prefix).
67    SfxHeader,
68    /// KotOR VO-prefixed wrapper (20-byte prefix).
69    VoHeader,
70    /// MP3-in-WAV wrapper (58-byte prefix and raw MP3 payload).
71    Mp3InWav,
72}
73
74/// KotOR-facing wrapper type used when writing.
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
76pub enum WavType {
77    /// No KotOR wrapper -- plain RIFF/WAVE payload written and read as-is.
78    Standard,
79    /// Voice-over style wrapper (20-byte prefix starting with `RIFF`).
80    Vo,
81    /// Sound-effect style wrapper (470-byte prefix starting with `FF F3 60 C4`).
82    Sfx,
83}
84
85/// Audio payload format represented by [`Wav`].
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
87pub enum WavAudioFormat {
88    /// Standard RIFF/WAVE payload.
89    Wave,
90    /// Raw MP3 payload bytes.
91    Mp3,
92}
93
94/// Known WAVE encoding tags.
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
96#[repr(u16)]
97pub enum WavEncoding {
98    /// Linear PCM.
99    Pcm = 0x0001,
100    /// Microsoft ADPCM.
101    MsAdpcm = 0x0002,
102    /// A-Law companded PCM.
103    ALaw = 0x0006,
104    /// Mu-Law companded PCM.
105    MuLaw = 0x0007,
106    /// IMA ADPCM (DVI ADPCM).
107    ImaAdpcm = 0x0011,
108    /// MPEG Layer 3 payload tag.
109    Mp3 = 0x0055,
110}
111
112/// Lossless WAVE encoding tag wrapper.
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
114pub struct WavEncodingCode(u16);
115
116impl WavEncodingCode {
117    /// Creates an encoding code from a raw on-disk tag value.
118    pub const fn from_raw(raw: u16) -> Self {
119        Self(raw)
120    }
121
122    /// Returns the raw encoding tag value.
123    pub const fn raw(self) -> u16 {
124        self.0
125    }
126
127    /// Returns the known encoding tag when available.
128    pub fn known(self) -> Option<WavEncoding> {
129        WavEncoding::try_from(self.0).ok()
130    }
131}
132
133impl From<WavEncoding> for WavEncodingCode {
134    fn from(value: WavEncoding) -> Self {
135        Self(u16::from(value))
136    }
137}
138
139/// In-memory WAV payload.
140#[derive(Debug, Clone, PartialEq, Eq)]
141pub struct Wav {
142    /// Wrapper mode used when writing game-facing bytes.
143    pub wav_type: WavType,
144    /// Audio payload kind.
145    pub audio_format: WavAudioFormat,
146    /// WAVE encoding tag (lossless raw value).
147    pub encoding: WavEncodingCode,
148    /// Channel count.
149    pub channels: u16,
150    /// Sample rate in Hz.
151    pub sample_rate: u32,
152    /// Byte rate from `fmt` chunk.
153    pub bytes_per_sec: u32,
154    /// Block alignment from `fmt` chunk.
155    pub block_align: u16,
156    /// Bits per sample from `fmt` chunk.
157    pub bits_per_sample: u16,
158    /// Audio payload bytes (PCM/ADPCM/MP3 data).
159    pub data: Vec<u8>,
160}
161
162/// Metadata fields for WAVE-format payloads.
163#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
164pub struct WavWaveMetadata {
165    /// WAVE encoding tag (lossless raw value).
166    pub encoding: WavEncodingCode,
167    /// Channel count.
168    pub channels: u16,
169    /// Sample rate in Hz.
170    pub sample_rate: u32,
171    /// Byte rate from `fmt` chunk.
172    pub bytes_per_sec: u32,
173    /// Block alignment from `fmt` chunk.
174    pub block_align: u16,
175    /// Bits per sample from `fmt` chunk.
176    pub bits_per_sample: u16,
177}
178
179impl Wav {
180    /// Creates a WAVE-format container payload.
181    pub fn new_wave(wav_type: WavType, metadata: WavWaveMetadata, data: Vec<u8>) -> Self {
182        Self {
183            wav_type,
184            audio_format: WavAudioFormat::Wave,
185            encoding: metadata.encoding,
186            channels: metadata.channels,
187            sample_rate: metadata.sample_rate,
188            bytes_per_sec: metadata.bytes_per_sec,
189            block_align: metadata.block_align,
190            bits_per_sample: metadata.bits_per_sample,
191            data,
192        }
193    }
194
195    /// Creates an MP3 payload container with canonical metadata defaults.
196    pub fn new_mp3(wav_type: WavType, data: Vec<u8>) -> Self {
197        Self {
198            wav_type,
199            audio_format: WavAudioFormat::Mp3,
200            encoding: WavEncodingCode::from(WavEncoding::Mp3),
201            channels: DEFAULT_MP3_CHANNELS,
202            sample_rate: DEFAULT_MP3_SAMPLE_RATE,
203            bytes_per_sec: 0,
204            block_align: 0,
205            bits_per_sample: DEFAULT_MP3_BITS_PER_SAMPLE,
206            data,
207        }
208    }
209}
210
211impl DecodeBinary for Wav {
212    type Error = WavError;
213
214    fn decode_binary(bytes: &[u8]) -> Result<Self, Self::Error> {
215        read_wav_from_bytes(bytes)
216    }
217}
218
219impl EncodeBinary for Wav {
220    type Error = WavError;
221
222    fn encode_binary(&self) -> Result<Vec<u8>, Self::Error> {
223        write_wav_to_vec(self)
224    }
225}
226
227/// WAV write target mode.
228#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
229pub enum WavWriteMode {
230    /// Emit KotOR game-facing wrapper style based on [`Wav::wav_type`].
231    #[default]
232    Game,
233    /// Emit clean playable bytes (plain RIFF/WAVE or raw MP3 bytes).
234    Clean,
235}
236
237/// WAV writer options.
238#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
239pub struct WavWriteOptions {
240    /// Output mode.
241    pub mode: WavWriteMode,
242}
243
244/// WAV parse/write errors.
245#[derive(Debug, Error)]
246pub enum WavError {
247    /// I/O read/write failure.
248    #[error(transparent)]
249    Io(#[from] std::io::Error),
250    /// Header-level validation failure.
251    #[error("invalid WAV header: {0}")]
252    InvalidHeader(String),
253    /// RIFF chunk-level validation failure.
254    #[error("invalid WAV chunk: {0}")]
255    InvalidChunk(String),
256}