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