Skip to main content

rakata_formats/wav/
pcm.rs

1//! PCM decoding helpers for WAV data.
2//!
3//! This module provides utilities to decode raw PCM sample data from
4//! [`super::Wav`] payloads into normalized floating-point samples.
5
6use super::{Wav, WavEncoding};
7use thiserror::Error;
8
9/// Errors that can occur during PCM decoding.
10#[derive(Debug, Error)]
11pub enum PcmError {
12    /// The WAV data is not in a supported PCM format.
13    #[error("unsupported encoding: {0:?}")]
14    UnsupportedEncoding(Option<WavEncoding>),
15
16    /// The bit depth is not supported for the given encoding.
17    #[error("unsupported bit depth: {0}")]
18    UnsupportedBitDepth(u16),
19
20    /// The data length is not aligned with the block size.
21    #[error("data length {0} is not a multiple of block alignment {1}")]
22    MisalignedData(usize, usize),
23}
24
25/// Decodes the audio data into a vector of normalized `f32` samples.
26///
27/// Samples are interleaved (e.g., L, R, L, R for stereo).
28/// Normalized samples are in the range `[-1.0, 1.0]`.
29///
30/// # Errors
31///
32/// [`PcmError::UnsupportedEncoding`] when the `fmt` chunk does not declare
33/// plain PCM, [`PcmError::UnsupportedBitDepth`] for a width this decoder has
34/// no conversion for, and [`PcmError::MisalignedData`] when the data length is
35/// not a whole number of samples.
36pub fn decode_pcm_as_float(wav: &Wav) -> Result<Vec<f32>, PcmError> {
37    let encoding = wav.encoding.known();
38
39    if encoding != Some(WavEncoding::Pcm) {
40        return Err(PcmError::UnsupportedEncoding(encoding));
41    }
42
43    let channels = usize::from(wav.channels);
44    if channels == 0 {
45        return Ok(Vec::new());
46    }
47
48    let bits = wav.bits_per_sample;
49    let bytes_per_sample = usize::from(bits / 8);
50    let block_align = channels * bytes_per_sample;
51
52    if !wav.data.len().is_multiple_of(block_align) {
53        return Err(PcmError::MisalignedData(wav.data.len(), block_align));
54    }
55
56    let num_samples = wav.data.len() / bytes_per_sample;
57    let mut samples = Vec::with_capacity(num_samples);
58
59    match bits {
60        8 => {
61            // 8-bit PCM is unsigned: 0..255, silence at 128.
62            // Range: [0, 255] -> [-1.0, 1.0]
63            for &byte in &wav.data {
64                let sample = (f32::from(byte) - 128.0) / 128.0;
65                samples.push(sample);
66            }
67        }
68        16 => {
69            // 16-bit PCM is signed little-endian: -32768..32767.
70            // Range: [-32768, 32767] -> [-1.0, 1.0]
71            for chunk in wav.data.chunks_exact(2) {
72                let sample_i16 = i16::from_le_bytes([chunk[0], chunk[1]]);
73                let sample = f32::from(sample_i16) / 32768.0;
74                samples.push(sample);
75            }
76        }
77        _ => return Err(PcmError::UnsupportedBitDepth(bits)),
78    }
79
80    Ok(samples)
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use crate::wav::{WavEncodingCode, WavType, WavWaveMetadata};
87
88    fn make_wav(bits: u16, data: Vec<u8>) -> Wav {
89        Wav::new_wave(
90            WavType::Vo,
91            WavWaveMetadata {
92                encoding: WavEncodingCode::from(WavEncoding::Pcm),
93                channels: 1,
94                sample_rate: 44100,
95                bytes_per_sec: 44100 * (u32::from(bits) / 8),
96                block_align: bits / 8,
97                bits_per_sample: bits,
98            },
99            data,
100        )
101    }
102
103    #[test]
104    fn test_decode_8bit_pcm() {
105        // 128 is silence (0.0), 0 is -1.0, 255 is approx 1.0
106        let data = vec![128, 0, 255];
107        let wav = make_wav(8, data);
108        let samples = decode_pcm_as_float(&wav).unwrap();
109
110        assert_eq!(samples.len(), 3);
111        assert!((samples[0] - 0.0).abs() < 1e-5);
112        assert!((samples[1] - -1.0).abs() < 1e-5);
113        assert!((samples[2] - 0.9921875).abs() < 1e-5);
114    }
115
116    #[test]
117    fn test_decode_16bit_pcm() {
118        // 0 is silence, i16::MIN is -1.0, i16::MAX is approx 1.0
119        let data = vec![
120            0x00, 0x00, // 0
121            0x00, 0x80, // -32768 (i16::MIN)
122            0xFF, 0x7F, // 32767 (i16::MAX)
123        ];
124        let wav = make_wav(16, data);
125        let samples = decode_pcm_as_float(&wav).unwrap();
126
127        assert_eq!(samples.len(), 3);
128        assert!((samples[0] - 0.0).abs() < 1e-5);
129        assert!((samples[1] - -1.0).abs() < 1e-5);
130        assert!((samples[2] - 0.9999695).abs() < 1e-5);
131    }
132
133    #[test]
134    fn test_unsupported_encoding() {
135        let mut wav = make_wav(16, vec![]);
136        wav.encoding = WavEncodingCode::from(WavEncoding::Mp3); // Not PCM
137        let err = decode_pcm_as_float(&wav).unwrap_err();
138        assert!(matches!(err, PcmError::UnsupportedEncoding(_)));
139    }
140}