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(
30            data[pos + 4..pos + 8]
31                .try_into()
32                .map_err(|_| "Invalid chunk size bytes in WAV file")?,
33        ) as usize;
34        pos += 8;
35
36        if pos + chunk_size > data.len() {
37            break;
38        }
39
40        match chunk_id {
41            b"fmt " => {
42                if chunk_size < 16 {
43                    return Err("fmt chunk too small".into());
44                }
45
46                let audio_format = u16::from_le_bytes(
47                    data[pos..pos + 2]
48                        .try_into()
49                        .map_err(|_| "Invalid audio format bytes in WAV file")?,
50                );
51                channels = u16::from_le_bytes(
52                    data[pos + 2..pos + 4]
53                        .try_into()
54                        .map_err(|_| "Invalid channel count bytes in WAV file")?,
55                );
56                sample_rate = u32::from_le_bytes(
57                    data[pos + 4..pos + 8]
58                        .try_into()
59                        .map_err(|_| "Invalid sample rate bytes in WAV file")?,
60                );
61                bits = u16::from_le_bytes(
62                    data[pos + 14..pos + 16]
63                        .try_into()
64                        .map_err(|_| "Invalid bit depth bytes in WAV file")?,
65                );
66
67                if audio_format != 1 {
68                    return Err("Only uncompressed PCM supported".into());
69                }
70
71                if !(bits == 8 || bits == 16 || bits == 24 || bits == 32) {
72                    return Err(format!(
73                        "Unsupported bit depth {} (expected 8/16/24/32)",
74                        bits
75                    ));
76                }
77            }
78            b"data" => {
79                raw_bytes = Some(data[pos..pos + chunk_size].to_vec());
80            }
81            _ => { /* Ignore other chunks */ }
82        }
83
84        pos += chunk_size;
85    }
86
87    let bytes = raw_bytes.ok_or("data chunk not found".to_string())?;
88
89    // Convert to f32 based on bit depth
90    let mut interleaved_f32: Vec<f32> = Vec::new();
91
92    match bits {
93        8 => {
94            // 8-bit: unsigned, range [0, 255] → [-1.0, 1.0]
95            for b in bytes.iter() {
96                interleaved_f32.push((*b as f32 - 128.0) / 128.0);
97            }
98        }
99        16 => {
100            // 16-bit: signed, range [-32768, 32767] → [-1.0, 1.0]
101            for ch in bytes.chunks_exact(2) {
102                let v = i16::from_le_bytes([ch[0], ch[1]]);
103                interleaved_f32.push(v as f32 / 32768.0);
104            }
105        }
106        24 => {
107            // 24-bit: signed, range [-8388608, 8388607] → [-1.0, 1.0]
108            for ch in bytes.chunks_exact(3) {
109                let assembled = (ch[0] as u32) | ((ch[1] as u32) << 8) | ((ch[2] as u32) << 16);
110
111                // Sign extend from 24-bit to 32-bit
112                let signed = if (assembled & 0x800000) != 0 {
113                    (assembled | 0xFF000000) as i32
114                } else {
115                    assembled as i32
116                };
117
118                interleaved_f32.push(signed as f32 / 8388608.0);
119            }
120        }
121        32 => {
122            // 32-bit: signed, range [-2147483648, 2147483647] → [-1.0, 1.0]
123            for ch in bytes.chunks_exact(4) {
124                let v = i32::from_le_bytes([ch[0], ch[1], ch[2], ch[3]]);
125                interleaved_f32.push(v as f32 / 2147483648.0);
126            }
127        }
128        _ => return Err("Unexpected bit depth".into()),
129    }
130
131    let chn = channels as usize;
132
133    // Convert stereo to mono if needed
134    if chn > 1 {
135        let frames = interleaved_f32.len() / chn;
136        let mut mono_f32 = Vec::with_capacity(frames);
137
138        for f in 0..frames {
139            let mut acc = 0.0;
140            for c in 0..chn {
141                acc += interleaved_f32[f * chn + c];
142            }
143            mono_f32.push(acc / chn as f32);
144        }
145
146        // Convert to i16
147        let mut out = Vec::with_capacity(mono_f32.len());
148        for s in mono_f32 {
149            out.push((s.clamp(-1.0, 1.0) * 32767.0) as i16);
150        }
151
152        Ok((1, sample_rate, out))
153    } else {
154        // Already mono, just convert to i16
155        let mut out = Vec::with_capacity(interleaved_f32.len());
156        for s in interleaved_f32 {
157            out.push((s.clamp(-1.0, 1.0) * 32767.0) as i16);
158        }
159
160        Ok((1, sample_rate, out))
161    }
162}
163
164#[cfg(test)]
165#[path = "test_wav_parser.rs"]
166mod tests;