devalang_wasm/utils/
wav_parser.rs

1//! WAV file parsing utilities
2//!
3//! Parses WAV files directly in Rust without requiring Web Audio API.
4//! Supports 8/16/24/32-bit PCM, mono/stereo, and automatic stereo→mono conversion.
5
6/// Parse WAV file and return (channels, sample_rate, mono_i16_samples)
7///
8/// If stereo, automatically converts to mono by averaging channels.
9/// Returns samples as i16 normalized to [-32768, 32767] range.
10pub fn parse_wav_generic(data: &[u8]) -> Result<(u16, u32, Vec<i16>), String> {
11    if data.len() < 44 {
12        return Err("File too short for WAV header".into());
13    }
14
15    // Validate RIFF/WAVE header
16    if &data[0..4] != b"RIFF" || &data[8..12] != b"WAVE" {
17        return Err("Invalid RIFF/WAVE header".into());
18    }
19
20    let mut pos = 12; // After RIFF header
21    let mut channels = 1u16;
22    let mut sample_rate = 44100u32;
23    let mut bits = 16u16;
24    let mut raw_bytes: Option<Vec<u8>> = None;
25
26    // Parse chunks
27    while pos + 8 <= data.len() {
28        let chunk_id = &data[pos..pos + 4];
29        let chunk_size = u32::from_le_bytes(data[pos + 4..pos + 8].try_into().unwrap()) as usize;
30        pos += 8;
31
32        if pos + chunk_size > data.len() {
33            break;
34        }
35
36        match chunk_id {
37            b"fmt " => {
38                if chunk_size < 16 {
39                    return Err("fmt chunk too small".into());
40                }
41
42                let audio_format = u16::from_le_bytes(data[pos..pos + 2].try_into().unwrap());
43                channels = u16::from_le_bytes(data[pos + 2..pos + 4].try_into().unwrap());
44                sample_rate = u32::from_le_bytes(data[pos + 4..pos + 8].try_into().unwrap());
45                bits = u16::from_le_bytes(data[pos + 14..pos + 16].try_into().unwrap());
46
47                if audio_format != 1 {
48                    return Err("Only uncompressed PCM supported".into());
49                }
50
51                if !(bits == 8 || bits == 16 || bits == 24 || bits == 32) {
52                    return Err(format!(
53                        "Unsupported bit depth {} (expected 8/16/24/32)",
54                        bits
55                    ));
56                }
57            }
58            b"data" => {
59                raw_bytes = Some(data[pos..pos + chunk_size].to_vec());
60            }
61            _ => { /* Ignore other chunks */ }
62        }
63
64        pos += chunk_size;
65    }
66
67    let bytes = raw_bytes.ok_or("data chunk not found".to_string())?;
68
69    // Convert to f32 based on bit depth
70    let mut interleaved_f32: Vec<f32> = Vec::new();
71
72    match bits {
73        8 => {
74            // 8-bit: unsigned, range [0, 255] → [-1.0, 1.0]
75            for b in bytes.iter() {
76                interleaved_f32.push((*b as f32 - 128.0) / 128.0);
77            }
78        }
79        16 => {
80            // 16-bit: signed, range [-32768, 32767] → [-1.0, 1.0]
81            for ch in bytes.chunks_exact(2) {
82                let v = i16::from_le_bytes([ch[0], ch[1]]);
83                interleaved_f32.push(v as f32 / 32768.0);
84            }
85        }
86        24 => {
87            // 24-bit: signed, range [-8388608, 8388607] → [-1.0, 1.0]
88            for ch in bytes.chunks_exact(3) {
89                let assembled = (ch[0] as u32) | ((ch[1] as u32) << 8) | ((ch[2] as u32) << 16);
90
91                // Sign extend from 24-bit to 32-bit
92                let signed = if (assembled & 0x800000) != 0 {
93                    (assembled | 0xFF000000) as i32
94                } else {
95                    assembled as i32
96                };
97
98                interleaved_f32.push(signed as f32 / 8388608.0);
99            }
100        }
101        32 => {
102            // 32-bit: signed, range [-2147483648, 2147483647] → [-1.0, 1.0]
103            for ch in bytes.chunks_exact(4) {
104                let v = i32::from_le_bytes([ch[0], ch[1], ch[2], ch[3]]);
105                interleaved_f32.push(v as f32 / 2147483648.0);
106            }
107        }
108        _ => return Err("Unexpected bit depth".into()),
109    }
110
111    let chn = channels as usize;
112
113    // Convert stereo to mono if needed
114    if chn > 1 {
115        let frames = interleaved_f32.len() / chn;
116        let mut mono_f32 = Vec::with_capacity(frames);
117
118        for f in 0..frames {
119            let mut acc = 0.0;
120            for c in 0..chn {
121                acc += interleaved_f32[f * chn + c];
122            }
123            mono_f32.push(acc / chn as f32);
124        }
125
126        // Convert to i16
127        let mut out = Vec::with_capacity(mono_f32.len());
128        for s in mono_f32 {
129            out.push((s.clamp(-1.0, 1.0) * 32767.0) as i16);
130        }
131
132        Ok((1, sample_rate, out))
133    } else {
134        // Already mono, just convert to i16
135        let mut out = Vec::with_capacity(interleaved_f32.len());
136        for s in interleaved_f32 {
137            out.push((s.clamp(-1.0, 1.0) * 32767.0) as i16);
138        }
139
140        Ok((1, sample_rate, out))
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn test_invalid_wav() {
150        let data = vec![0u8; 10];
151        assert!(parse_wav_generic(&data).is_err());
152    }
153
154    #[test]
155    fn test_valid_wav_header() {
156        // Minimal WAV header (44 bytes)
157        let mut data = vec![0u8; 44];
158        data[0..4].copy_from_slice(b"RIFF");
159        data[8..12].copy_from_slice(b"WAVE");
160        data[12..16].copy_from_slice(b"fmt ");
161        data[16..20].copy_from_slice(&16u32.to_le_bytes()); // fmt size
162        data[20..22].copy_from_slice(&1u16.to_le_bytes()); // PCM
163        data[22..24].copy_from_slice(&1u16.to_le_bytes()); // mono
164        data[24..28].copy_from_slice(&44100u32.to_le_bytes()); // sample rate
165        data[34..36].copy_from_slice(&16u16.to_le_bytes()); // bit depth
166        data[36..40].copy_from_slice(b"data");
167        data[40..44].copy_from_slice(&0u32.to_le_bytes()); // data size
168
169        let result = parse_wav_generic(&data);
170        assert!(result.is_ok());
171    }
172}