Skip to main content

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