rakata_formats/wav/
adpcm.rs

1//! ADPCM decoding helpers for WAV data.
2//!
3//! This module provides utilities to decode ADPCM sample data from
4//! [`super::Wav`] payloads into normalized floating-point samples.
5//!
6//! Supported formats:
7//! - MS ADPCM (0x0002)
8//! - IMA ADPCM (0x0011)
9
10use super::{Wav, WavEncoding};
11use thiserror::Error;
12
13/// Normalizes a clamped i32 sample (must be in i16 range) to f32 in `[-1.0, 1.0]`.
14///
15/// The conversion is lossless: `i16 -> f32` is exact (f32 has a 24-bit mantissa).
16fn normalize_sample(sample: i32) -> f32 {
17    let narrow = i16::try_from(sample).expect("ADPCM samples are clamped to i16 range");
18    f32::from(narrow) / 32768.0
19}
20
21/// Errors that can occur during ADPCM decoding.
22#[derive(Debug, Error)]
23pub enum AdpcmError {
24    /// The WAV data is not in a supported ADPCM format.
25    #[error("unsupported encoding: {0:?}")]
26    UnsupportedEncoding(Option<WavEncoding>),
27
28    /// The block alignment is invalid or zero.
29    #[error("invalid block align: {0}")]
30    InvalidBlockAlign(u16),
31
32    /// A required coefficient table is missing or invalid.
33    #[error("invalid or missing coefficient table")]
34    InvalidCoefficients,
35
36    /// The data length is insufficient for the declared block structure.
37    #[error("unexpected end of data")]
38    UnexpectedEof,
39}
40
41/// Decodes the audio data into a vector of normalized `f32` samples.
42///
43/// Samples are interleaved (e.g., L, R, L, R for stereo).
44/// Normalized samples are in the range `[-1.0, 1.0]`.
45pub fn decode_adpcm_as_float(wav: &Wav) -> Result<Vec<f32>, AdpcmError> {
46    let encoding = wav.encoding.known();
47
48    match encoding {
49        Some(WavEncoding::MsAdpcm) => decode_ms_adpcm(wav),
50        Some(WavEncoding::ImaAdpcm) => decode_ima_adpcm(wav),
51        _ => Err(AdpcmError::UnsupportedEncoding(encoding)),
52    }
53}
54
55// ============================================================================
56// MS ADPCM Implementation
57// ============================================================================
58
59/// Step adaptation table: scales the quantization delta after each nibble.
60/// Indexed by the raw (unsigned) nibble value [0..15].
61const MS_ADAPTATION_TABLE: [i32; 16] = [
62    230, 230, 230, 230, 307, 409, 512, 614, 768, 614, 512, 409, 307, 230, 230, 230,
63];
64
65fn decode_ms_adpcm(wav: &Wav) -> Result<Vec<f32>, AdpcmError> {
66    let channels = usize::from(wav.channels);
67    let block_align = usize::from(wav.block_align);
68
69    if block_align == 0 {
70        return Err(AdpcmError::InvalidBlockAlign(0));
71    }
72
73    // The `fmt` chunk extra bytes (where custom predictor coefficients live) are not
74    // stored in the `Wav` struct, so we fall back to the 7 standard MS ADPCM coefficients.
75    // KotOR audio files universally use these standard coefficients.
76    //
77    // TODO: store extra `fmt` bytes in `Wav` to support non-standard coefficient tables.
78    let standard_coeffs = [
79        (256, 0),
80        (512, -256),
81        (0, 0),
82        (192, 64),
83        (240, 0),
84        (460, -208),
85        (392, -232),
86    ];
87
88    let num_blocks = wav.data.len() / block_align;
89    let samples_per_block = ms_adpcm_samples_per_block(channels, block_align)?;
90    let mut output = Vec::with_capacity(num_blocks * samples_per_block * channels);
91
92    for block_idx in 0..num_blocks {
93        let block_offset = block_idx * block_align;
94        let block = &wav.data[block_offset..block_offset + block_align];
95
96        if channels == 1 {
97            decode_ms_adpcm_block_mono(block, &standard_coeffs, &mut output)?;
98        } else if channels == 2 {
99            decode_ms_adpcm_block_stereo(block, &standard_coeffs, &mut output)?;
100        } else {
101            return Err(AdpcmError::UnsupportedEncoding(None));
102        }
103    }
104
105    Ok(output)
106}
107
108fn ms_adpcm_samples_per_block(channels: usize, block_align: usize) -> Result<usize, AdpcmError> {
109    // Each channel header is 7 bytes: 1 predictor + 2-byte iDelta + 2×2-byte history samples.
110    // Remaining bytes hold nibble-packed data at 2 samples per byte per channel.
111    // The 2 history samples from the header are also emitted, giving the +2 below.
112    let overhead = 7 * channels;
113    if block_align < overhead {
114        return Err(AdpcmError::InvalidBlockAlign(
115            u16::try_from(block_align).expect("block_align originates from a u16 field"),
116        ));
117    }
118    Ok(2 + (block_align - overhead) * 2 / channels)
119}
120
121fn decode_ms_adpcm_block_mono(
122    block: &[u8],
123    coeffs: &[(i32, i32)],
124    output: &mut Vec<f32>,
125) -> Result<(), AdpcmError> {
126    if block.len() < 7 {
127        return Err(AdpcmError::UnexpectedEof);
128    }
129
130    let predictor = usize::from(block[0]);
131    if predictor >= coeffs.len() {
132        return Err(AdpcmError::InvalidCoefficients);
133    }
134    let (c1, c2) = coeffs[predictor];
135
136    let mut delta = i32::from(i16::from_le_bytes([block[1], block[2]]));
137    // samp2 is the older history sample (n−2); samp1 is more recent (n−1).
138    let mut samp1 = i32::from(i16::from_le_bytes([block[3], block[4]]));
139    let mut samp2 = i32::from(i16::from_le_bytes([block[5], block[6]]));
140
141    // Emit the two history samples in chronological order before the coded data.
142    output.push(normalize_sample(samp2));
143    output.push(normalize_sample(samp1));
144
145    // Each byte holds two samples: high nibble first, then low nibble.
146    for byte in &block[7..] {
147        for nibble in [i32::from(byte >> 4), i32::from(byte & 0x0F)] {
148            let pred = (samp1 * c1 + samp2 * c2) / 256;
149
150            // Sign-extend the 4-bit nibble: values [8, 15] map to [-8, -1].
151            let signed_nibble = if nibble >= 8 { nibble - 16 } else { nibble };
152            let clamped = (pred + signed_nibble * delta).clamp(-32768, 32767);
153
154            delta = (delta
155                * MS_ADAPTATION_TABLE[usize::try_from(nibble).expect("nibble is 0..15")]
156                / 256)
157                .max(16);
158
159            // Advance history window.
160            samp2 = samp1;
161            samp1 = clamped;
162
163            output.push(normalize_sample(samp1));
164        }
165    }
166    Ok(())
167}
168
169fn decode_ms_adpcm_block_stereo(
170    block: &[u8],
171    coeffs: &[(i32, i32)],
172    output: &mut Vec<f32>,
173) -> Result<(), AdpcmError> {
174    if block.len() < 14 {
175        return Err(AdpcmError::UnexpectedEof);
176    }
177
178    let pred_l = usize::from(block[0]);
179    let pred_r = usize::from(block[1]);
180    if pred_l >= coeffs.len() || pred_r >= coeffs.len() {
181        return Err(AdpcmError::InvalidCoefficients);
182    }
183    let (c1_l, c2_l) = coeffs[pred_l];
184    let (c1_r, c2_r) = coeffs[pred_r];
185
186    let mut delta_l = i32::from(i16::from_le_bytes([block[2], block[3]]));
187    let mut delta_r = i32::from(i16::from_le_bytes([block[4], block[5]]));
188
189    // samp1 is the more recent history sample (n−1); samp2 is older (n−2).
190    let mut samp1_l = i32::from(i16::from_le_bytes([block[6], block[7]]));
191    let mut samp1_r = i32::from(i16::from_le_bytes([block[8], block[9]]));
192    let mut samp2_l = i32::from(i16::from_le_bytes([block[10], block[11]]));
193    let mut samp2_r = i32::from(i16::from_le_bytes([block[12], block[13]]));
194
195    // Emit the two history pairs in chronological order (samp2 then samp1), interleaved L/R.
196    output.push(normalize_sample(samp2_l));
197    output.push(normalize_sample(samp2_r));
198    output.push(normalize_sample(samp1_l));
199    output.push(normalize_sample(samp1_r));
200
201    // Each byte holds one left nibble (high) and one right nibble (low).
202    for byte in &block[14..] {
203        let nibble_l = i32::from(byte >> 4);
204        let nibble_r = i32::from(byte & 0x0F);
205
206        // Left channel.
207        let pred = (samp1_l * c1_l + samp2_l * c2_l) / 256;
208        let signed_nibble = if nibble_l >= 8 {
209            nibble_l - 16
210        } else {
211            nibble_l
212        };
213        let clamped_l = (pred + signed_nibble * delta_l).clamp(-32768, 32767);
214        delta_l = (delta_l
215            * MS_ADAPTATION_TABLE[usize::try_from(nibble_l).expect("nibble is 0..15")]
216            / 256)
217            .max(16);
218        samp2_l = samp1_l;
219        samp1_l = clamped_l;
220
221        // Right channel.
222        let pred = (samp1_r * c1_r + samp2_r * c2_r) / 256;
223        let signed_nibble = if nibble_r >= 8 {
224            nibble_r - 16
225        } else {
226            nibble_r
227        };
228        let clamped_r = (pred + signed_nibble * delta_r).clamp(-32768, 32767);
229        delta_r = (delta_r
230            * MS_ADAPTATION_TABLE[usize::try_from(nibble_r).expect("nibble is 0..15")]
231            / 256)
232            .max(16);
233        samp2_r = samp1_r;
234        samp1_r = clamped_r;
235
236        output.push(normalize_sample(clamped_l));
237        output.push(normalize_sample(clamped_r));
238    }
239    Ok(())
240}
241
242// ============================================================================
243// IMA ADPCM Implementation
244// ============================================================================
245
246const IMA_INDEX_TABLE: [i8; 16] = [-1, -1, -1, -1, 2, 4, 6, 8, -1, -1, -1, -1, 2, 4, 6, 8];
247
248const IMA_STEP_TABLE: [i32; 89] = [
249    7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 19, 21, 23, 25, 28, 31, 34, 37, 41, 45, 50, 55, 60, 66,
250    73, 80, 88, 97, 107, 118, 130, 143, 157, 173, 190, 209, 230, 253, 279, 307, 337, 371, 408, 449,
251    494, 544, 598, 658, 724, 796, 876, 963, 1060, 1166, 1282, 1411, 1552, 1707, 1878, 2066, 2272,
252    2499, 2749, 3024, 3327, 3660, 4026, 4428, 4871, 5358, 5894, 6484, 7132, 7845, 8630, 9493,
253    10442, 11487, 12635, 13899, 15290, 16818, 18500, 20350, 22385, 24623, 27086, 29794, 32767,
254];
255
256fn decode_ima_adpcm(wav: &Wav) -> Result<Vec<f32>, AdpcmError> {
257    let channels = usize::from(wav.channels);
258    let block_align = usize::from(wav.block_align);
259
260    let header_bytes = 4 * channels;
261    if block_align < header_bytes {
262        return Err(AdpcmError::InvalidBlockAlign(wav.block_align));
263    }
264
265    // Number of samples per block per channel:
266    // Header is 4 bytes per channel (predictor i16, step_index u8, reserved u8).
267    // Remaining bytes hold 2 samples per byte per channel.
268    // +1 accounts for the sample stored in the header itself.
269    let samples_per_block = (block_align - header_bytes) * 2 / channels + 1;
270
271    let num_blocks = wav.data.len() / block_align;
272    let mut output = Vec::with_capacity(num_blocks * samples_per_block * channels);
273
274    for block_idx in 0..num_blocks {
275        let block_offset = block_idx * block_align;
276        let block = &wav.data[block_offset..block_offset + block_align];
277
278        if channels == 1 {
279            decode_ima_adpcm_block_mono(block, &mut output)?;
280        } else if channels == 2 {
281            decode_ima_adpcm_block_stereo(block, &mut output)?;
282        } else {
283            return Err(AdpcmError::UnsupportedEncoding(None));
284        }
285    }
286
287    Ok(output)
288}
289
290fn decode_ima_adpcm_block_mono(block: &[u8], output: &mut Vec<f32>) -> Result<(), AdpcmError> {
291    if block.len() < 4 {
292        return Err(AdpcmError::UnexpectedEof);
293    }
294
295    let mut predictor = i16::from_le_bytes([block[0], block[1]]).into();
296    let mut step_index = i32::from(block[2]).clamp(0, 88);
297    // block[3] is reserved; ignored.
298
299    output.push(normalize_sample(predictor));
300
301    // Each byte holds two samples: low nibble first, then high nibble.
302    for byte in &block[4..] {
303        for nibble in [i32::from(byte & 0x0F), i32::from(byte >> 4)] {
304            let step = IMA_STEP_TABLE
305                [usize::try_from(step_index).expect("step_index is clamped to 0..88")];
306            let mut diff = step >> 3;
307            if (nibble & 4) != 0 {
308                diff += step;
309            }
310            if (nibble & 2) != 0 {
311                diff += step >> 1;
312            }
313            if (nibble & 1) != 0 {
314                diff += step >> 2;
315            }
316
317            if (nibble & 8) != 0 {
318                predictor -= diff;
319            } else {
320                predictor += diff;
321            }
322            predictor = predictor.clamp(-32768, 32767);
323            step_index = (step_index
324                + i32::from(IMA_INDEX_TABLE[usize::try_from(nibble).expect("nibble is 0..15")]))
325            .clamp(0, 88);
326
327            output.push(normalize_sample(predictor));
328        }
329    }
330    Ok(())
331}
332
333fn decode_ima_adpcm_block_stereo(block: &[u8], output: &mut Vec<f32>) -> Result<(), AdpcmError> {
334    if block.len() < 8 {
335        return Err(AdpcmError::UnexpectedEof);
336    }
337
338    // Each channel has a 4-byte header: predictor i16, step_index u8, reserved u8.
339    let mut left_pred = i16::from_le_bytes([block[0], block[1]]).into();
340    let mut left_step_idx = i32::from(block[2]).clamp(0, 88);
341
342    let mut right_pred = i32::from(i16::from_le_bytes([block[4], block[5]]));
343    let mut right_step_idx = i32::from(block[6]).clamp(0, 88);
344
345    output.push(normalize_sample(left_pred));
346    output.push(normalize_sample(right_pred));
347
348    // Data is laid out as alternating 4-byte channel words: LLLL RRRR LLLL RRRR ...
349    // Each 8-byte chunk produces 8 left samples and 8 right samples, emitted interleaved.
350    let mut cursor = 8;
351    while cursor + 8 <= block.len() {
352        let left_chunk = &block[cursor..cursor + 4];
353        let right_chunk = &block[cursor + 4..cursor + 8];
354        cursor += 8;
355
356        let mut l_samples = [0f32; 8];
357        let mut r_samples = [0f32; 8];
358
359        for (i, &byte) in left_chunk.iter().enumerate() {
360            for (j, nibble) in [i32::from(byte & 0x0F), i32::from(byte >> 4)]
361                .into_iter()
362                .enumerate()
363            {
364                let step = IMA_STEP_TABLE
365                    [usize::try_from(left_step_idx).expect("step_index is clamped to 0..88")];
366                let mut diff = step >> 3;
367                if (nibble & 4) != 0 {
368                    diff += step;
369                }
370                if (nibble & 2) != 0 {
371                    diff += step >> 1;
372                }
373                if (nibble & 1) != 0 {
374                    diff += step >> 2;
375                }
376                if (nibble & 8) != 0 {
377                    left_pred -= diff;
378                } else {
379                    left_pred += diff;
380                }
381                left_pred = left_pred.clamp(-32768, 32767);
382                left_step_idx = (left_step_idx
383                    + i32::from(
384                        IMA_INDEX_TABLE[usize::try_from(nibble).expect("nibble is 0..15")],
385                    ))
386                .clamp(0, 88);
387                l_samples[i * 2 + j] = normalize_sample(left_pred);
388            }
389        }
390
391        for (i, &byte) in right_chunk.iter().enumerate() {
392            for (j, nibble) in [i32::from(byte & 0x0F), i32::from(byte >> 4)]
393                .into_iter()
394                .enumerate()
395            {
396                let step = IMA_STEP_TABLE
397                    [usize::try_from(right_step_idx).expect("step_index is clamped to 0..88")];
398                let mut diff = step >> 3;
399                if (nibble & 4) != 0 {
400                    diff += step;
401                }
402                if (nibble & 2) != 0 {
403                    diff += step >> 1;
404                }
405                if (nibble & 1) != 0 {
406                    diff += step >> 2;
407                }
408                if (nibble & 8) != 0 {
409                    right_pred -= diff;
410                } else {
411                    right_pred += diff;
412                }
413                right_pred = right_pred.clamp(-32768, 32767);
414                right_step_idx = (right_step_idx
415                    + i32::from(
416                        IMA_INDEX_TABLE[usize::try_from(nibble).expect("nibble is 0..15")],
417                    ))
418                .clamp(0, 88);
419                r_samples[i * 2 + j] = normalize_sample(right_pred);
420            }
421        }
422
423        for i in 0..8 {
424            output.push(l_samples[i]);
425            output.push(r_samples[i]);
426        }
427    }
428
429    Ok(())
430}
431
432#[cfg(test)]
433mod tests {
434    use super::*;
435    use crate::wav::{WavEncodingCode, WavType, WavWaveMetadata};
436
437    fn make_wav(encoding: WavEncoding, channels: u16, block_align: u16, data: Vec<u8>) -> Wav {
438        Wav::new_wave(
439            WavType::Vo,
440            WavWaveMetadata {
441                encoding: WavEncodingCode::from(encoding),
442                channels,
443                sample_rate: 44100,
444                bytes_per_sec: 0,
445                block_align,
446                bits_per_sample: 4,
447            },
448            data,
449        )
450    }
451
452    #[test]
453    fn test_ms_adpcm_mono_header() {
454        // Minimal MS ADPCM block: 7 bytes, predictor 0, delta 16, both history samples 0.
455        let data = vec![
456            0x00, // predictor index 0
457            0x10, 0x00, // iDelta = 16
458            0x00, 0x00, // iSamp1 = 0
459            0x00, 0x00, // iSamp2 = 0
460        ];
461        let wav = make_wav(WavEncoding::MsAdpcm, 1, 7, data);
462        let samples = decode_adpcm_as_float(&wav).unwrap();
463        assert_eq!(samples.len(), 2);
464        assert_eq!(samples[0], 0.0);
465        assert_eq!(samples[1], 0.0);
466    }
467
468    #[test]
469    fn test_ima_adpcm_mono_header() {
470        // Minimal IMA ADPCM block: 4 bytes, predictor 0, step index 0.
471        let data = vec![
472            0x00, 0x00, // predictor = 0
473            0x00, 0x00, // step_index = 0, reserved = 0
474        ];
475        let wav = make_wav(WavEncoding::ImaAdpcm, 1, 4, data);
476        let samples = decode_adpcm_as_float(&wav).unwrap();
477        assert_eq!(samples.len(), 1);
478        assert_eq!(samples[0], 0.0);
479    }
480
481    #[test]
482    fn test_unsupported_encoding() {
483        let wav = make_wav(WavEncoding::Pcm, 1, 2, vec![]);
484        let err = decode_adpcm_as_float(&wav).unwrap_err();
485        assert!(matches!(err, AdpcmError::UnsupportedEncoding(_)));
486    }
487}