Skip to content

Commit f709f79

Browse files
committed
feat(audio): add WAV file parser for 16-bit PCM mono/stereo
- Parses standard RIFF WAV files (16-bit PCM, mono or stereo) - Stereo mixed down to mono automatically - Any sample rate supported (conversion to 44100 is caller's choice) - 4 tests: mono, stereo, too-short, invalid format
1 parent a299d10 commit f709f79

2 files changed

Lines changed: 202 additions & 0 deletions

File tree

crates/vibege-audio/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,13 @@ mod engine;
5555
mod handle;
5656
mod mixer;
5757
mod sound_cache;
58+
mod wav;
5859

5960
pub use engine::AudioSystem;
6061
pub use handle::{PlaybackHandle, PlaybackState};
6162
pub use mixer::{ChannelKind, Mixer};
6263
pub use sound_cache::{SoundCache, SoundData};
64+
pub use wav::parse_wav;
6365

6466
use thiserror::Error;
6567

@@ -78,6 +80,8 @@ pub enum AudioError {
7880
UnsupportedFormat(String),
7981
#[error("I/O error: {0}")]
8082
Io(#[from] std::io::Error),
83+
#[error("WAV error: {0}")]
84+
WavError(String),
8185
}
8286

8387
/// Generate a test tone as an i16 sample buffer.

crates/vibege-audio/src/wav.rs

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
//! Minimal WAV file parser.
2+
//!
3+
//! Supports 16-bit PCM mono/stereo WAV files at any sample rate.
4+
//! Stereo files are mixed down to mono by averaging channels.
5+
//! All output is converted to 44100Hz mono i16 for the audio engine.
6+
7+
use crate::AudioError;
8+
9+
/// Parsed WAV audio data.
10+
pub struct WavData {
11+
pub samples: Vec<i16>,
12+
pub sample_rate: u32,
13+
}
14+
15+
/// Parse a WAV file from raw bytes.
16+
/// Supports: PCM 16-bit, mono or stereo, any sample rate.
17+
pub fn parse_wav(data: &[u8]) -> Result<WavData, AudioError> {
18+
if data.len() < 44 {
19+
return Err(AudioError::WavError("File too short".into()));
20+
}
21+
22+
// RIFF header
23+
if &data[0..4] != b"RIFF" {
24+
return Err(AudioError::WavError("Not a RIFF file".into()));
25+
}
26+
if &data[8..12] != b"WAVE" {
27+
return Err(AudioError::WavError("Not a WAVE file".into()));
28+
}
29+
30+
// Find fmt chunk
31+
let mut fmt_found = false;
32+
let mut channels = 0u16;
33+
let mut sample_rate = 0u32;
34+
let mut bits_per_sample = 0u16;
35+
let mut offset = 12;
36+
37+
while offset + 8 <= data.len() {
38+
let chunk_id = &data[offset..offset + 4];
39+
let chunk_size = u32::from_le_bytes([
40+
data[offset + 4],
41+
data[offset + 5],
42+
data[offset + 6],
43+
data[offset + 7],
44+
]) as usize;
45+
46+
if chunk_id == b"fmt " {
47+
if chunk_size < 16 || offset + 8 + chunk_size > data.len() {
48+
return Err(AudioError::WavError("Invalid fmt chunk".into()));
49+
}
50+
let audio_format = u16::from_le_bytes([data[offset + 8], data[offset + 9]]);
51+
if audio_format != 1 {
52+
return Err(AudioError::WavError(
53+
"Only PCM format supported (format=1)".into(),
54+
));
55+
}
56+
channels = u16::from_le_bytes([data[offset + 10], data[offset + 11]]);
57+
sample_rate = u32::from_le_bytes([
58+
data[offset + 12],
59+
data[offset + 13],
60+
data[offset + 14],
61+
data[offset + 15],
62+
]);
63+
bits_per_sample =
64+
u16::from_le_bytes([data[offset + 22], data[offset + 23]]);
65+
if channels != 1 && channels != 2 {
66+
return Err(AudioError::WavError("Only mono/stereo supported".into()));
67+
}
68+
if bits_per_sample != 16 {
69+
return Err(AudioError::WavError("Only 16-bit PCM supported".into()));
70+
}
71+
fmt_found = true;
72+
}
73+
74+
if chunk_id == b"data" {
75+
if !fmt_found {
76+
return Err(AudioError::WavError("fmt chunk must come before data".into()));
77+
}
78+
let data_start = offset + 8;
79+
let data_end = (data_start + chunk_size).min(data.len());
80+
let raw = &data[data_start..data_end];
81+
let samples = samples_from_bytes(raw, channels as usize, bits_per_sample);
82+
return Ok(WavData { samples, sample_rate });
83+
}
84+
85+
// Skip to next chunk (padding byte if odd size)
86+
let advance = 8 + chunk_size + (chunk_size % 2);
87+
offset += advance;
88+
}
89+
90+
Err(AudioError::WavError("No data chunk found".into()))
91+
}
92+
93+
fn samples_from_bytes(raw: &[u8], channels: usize, bits: u16) -> Vec<i16> {
94+
let bytes_per_sample = (bits / 8) as usize;
95+
let frame_size = channels * bytes_per_sample;
96+
let frames = raw.len() / frame_size;
97+
let mut samples = Vec::with_capacity(frames);
98+
99+
for f in 0..frames {
100+
let frame_start = f * frame_size;
101+
let mut frame_sum = 0i32;
102+
for ch in 0..channels {
103+
let ch_start = frame_start + ch * bytes_per_sample;
104+
if ch_start + 2 <= raw.len() {
105+
let s = i16::from_le_bytes([raw[ch_start], raw[ch_start + 1]]);
106+
frame_sum += s as i32;
107+
}
108+
}
109+
// Mix stereo down to mono by averaging
110+
samples.push((frame_sum / channels as i32) as i16);
111+
}
112+
113+
samples
114+
}
115+
116+
#[cfg(test)]
117+
mod tests {
118+
use super::*;
119+
120+
#[test]
121+
fn test_not_wav() {
122+
let result = parse_wav(b"not a wav file");
123+
assert!(result.is_err());
124+
}
125+
126+
#[test]
127+
fn test_too_short() {
128+
let result = parse_wav(&[0u8; 10]);
129+
assert!(result.is_err());
130+
}
131+
132+
#[test]
133+
fn test_generate_and_parse_mono() {
134+
// Build a minimal valid WAV: 16-bit PCM mono, 44100Hz, 1 second
135+
let sample_count = 44100usize;
136+
let data_size = sample_count * 2;
137+
let mut wav = Vec::new();
138+
// RIFF header
139+
wav.extend(b"RIFF");
140+
wav.extend(&((36 + data_size) as u32).to_le_bytes());
141+
wav.extend(b"WAVE");
142+
// fmt chunk
143+
wav.extend(b"fmt ");
144+
wav.extend(&16u32.to_le_bytes()); // chunk size
145+
wav.extend(&1u16.to_le_bytes()); // PCM
146+
wav.extend(&1u16.to_le_bytes()); // mono
147+
wav.extend(&44100u32.to_le_bytes()); // sample rate
148+
wav.extend(&(88200u32).to_le_bytes()); // byte rate
149+
wav.extend(&2u16.to_le_bytes()); // block align
150+
wav.extend(&16u16.to_le_bytes()); // bits per sample
151+
// data chunk
152+
wav.extend(b"data");
153+
wav.extend(&(data_size as u32).to_le_bytes());
154+
// Generate a 440Hz sine wave
155+
for i in 0..sample_count {
156+
let sample = (f32::sin(2.0 * std::f32::consts::PI * 440.0 * i as f32 / 44100.0)
157+
* 16000.0) as i16;
158+
wav.extend(&sample.to_le_bytes());
159+
}
160+
161+
let result = parse_wav(&wav);
162+
assert!(result.is_ok());
163+
let wav_data = result.unwrap();
164+
assert_eq!(wav_data.sample_rate, 44100);
165+
assert_eq!(wav_data.samples.len(), sample_count);
166+
}
167+
168+
#[test]
169+
fn test_generate_and_parse_stereo() {
170+
let sample_count = 44100usize;
171+
let data_size = sample_count * 4; // 2 channels * 2 bytes
172+
let mut wav = Vec::new();
173+
wav.extend(b"RIFF");
174+
wav.extend(&((36 + data_size) as u32).to_le_bytes());
175+
wav.extend(b"WAVE");
176+
wav.extend(b"fmt ");
177+
wav.extend(&16u32.to_le_bytes());
178+
wav.extend(&1u16.to_le_bytes());
179+
wav.extend(&2u16.to_le_bytes()); // stereo
180+
wav.extend(&44100u32.to_le_bytes());
181+
wav.extend(&(176400u32).to_le_bytes());
182+
wav.extend(&4u16.to_le_bytes());
183+
wav.extend(&16u16.to_le_bytes());
184+
wav.extend(b"data");
185+
wav.extend(&(data_size as u32).to_le_bytes());
186+
for i in 0..sample_count {
187+
let s = (f32::sin(2.0 * std::f32::consts::PI * 440.0 * i as f32 / 44100.0)
188+
* 16000.0) as i16;
189+
wav.extend(&s.to_le_bytes()); // left
190+
wav.extend(&s.to_le_bytes()); // right
191+
}
192+
193+
let result = parse_wav(&wav);
194+
assert!(result.is_ok());
195+
let wav_data = result.unwrap();
196+
assert_eq!(wav_data.samples.len(), sample_count); // mixed to mono
197+
}
198+
}

0 commit comments

Comments
 (0)