⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/audio/
wav.rs

1//! WAV file parser (PCM only)
2//!
3//! Parses RIFF/WAVE files and extracts PCM audio data. Supports common
4//! PCM formats (8-bit unsigned, 16-bit signed, 24-bit signed) and
5//! provides conversion utilities to normalize audio to S16Le for mixing.
6//!
7//! ## Supported Formats
8//!
9//! - PCM 8-bit unsigned (audio_format=1, bits_per_sample=8)
10//! - PCM 16-bit signed little-endian (audio_format=1, bits_per_sample=16)
11//! - PCM 24-bit signed little-endian (audio_format=1, bits_per_sample=24)
12//! - PCM 32-bit signed little-endian (audio_format=1, bits_per_sample=32)
13
14#![allow(dead_code)]
15
16use alloc::vec::Vec;
17
18use crate::{
19    audio::{AudioConfig, SampleFormat},
20    error::KernelError,
21};
22
23// ============================================================================
24// WAV File Constants
25// ============================================================================
26
27/// "RIFF" magic bytes
28const RIFF_MAGIC: [u8; 4] = [b'R', b'I', b'F', b'F'];
29
30/// "WAVE" format identifier
31const WAVE_MAGIC: [u8; 4] = [b'W', b'A', b'V', b'E'];
32
33/// "fmt " chunk identifier
34const FMT_CHUNK_ID: [u8; 4] = [b'f', b'm', b't', b' '];
35
36/// "data" chunk identifier
37const DATA_CHUNK_ID: [u8; 4] = [b'd', b'a', b't', b'a'];
38
39/// PCM audio format (uncompressed)
40const AUDIO_FORMAT_PCM: u16 = 1;
41
42// ============================================================================
43// WAV Header Structures
44// ============================================================================
45
46/// RIFF file header (12 bytes)
47#[derive(Debug, Clone, Copy)]
48pub struct WavHeader {
49    /// Must be "RIFF"
50    pub riff_magic: [u8; 4],
51    /// File size minus 8 bytes (RIFF header)
52    pub file_size: u32,
53    /// Must be "WAVE"
54    pub wave_magic: [u8; 4],
55}
56
57/// Format chunk describing the audio data layout
58#[derive(Debug, Clone, Copy)]
59pub struct FmtChunk {
60    /// Chunk identifier: "fmt "
61    pub chunk_id: [u8; 4],
62    /// Chunk data size (16 for PCM)
63    pub chunk_size: u32,
64    /// Audio format (1 = PCM)
65    pub audio_format: u16,
66    /// Number of channels (1 = mono, 2 = stereo)
67    pub num_channels: u16,
68    /// Samples per second (e.g., 44100, 48000)
69    pub sample_rate: u32,
70    /// Bytes per second (sample_rate * block_align)
71    pub byte_rate: u32,
72    /// Block alignment (num_channels * bits_per_sample / 8)
73    pub block_align: u16,
74    /// Bits per sample (8, 16, 24, 32)
75    pub bits_per_sample: u16,
76}
77
78// ============================================================================
79// Parsed WAV File
80// ============================================================================
81
82/// Parsed WAV file with metadata and data location
83#[derive(Debug, Clone)]
84pub struct WavFile {
85    /// Number of audio channels
86    pub num_channels: u16,
87    /// Sample rate in Hz
88    pub sample_rate: u32,
89    /// Bits per sample
90    pub bits_per_sample: u16,
91    /// Audio format code (1 = PCM)
92    pub audio_format: u16,
93    /// Byte offset to the start of PCM data in the source buffer
94    pub data_offset: usize,
95    /// Size of the PCM data section in bytes
96    pub data_size: usize,
97    /// Block alignment
98    pub block_align: u16,
99}
100
101impl WavFile {
102    /// Parse a WAV file from a byte buffer
103    ///
104    /// Validates the RIFF/WAVE header, locates the "fmt " and "data" chunks,
105    /// and extracts metadata.
106    pub fn parse(data: &[u8]) -> Result<WavFile, KernelError> {
107        // Minimum WAV file: 12 (RIFF header) + 24 (fmt chunk) + 8 (data header) = 44
108        if data.len() < 44 {
109            return Err(KernelError::InvalidArgument {
110                name: "wav_data",
111                value: "file too small for WAV header",
112            });
113        }
114
115        // Validate RIFF header
116        if data[0..4] != RIFF_MAGIC {
117            return Err(KernelError::InvalidArgument {
118                name: "wav_data",
119                value: "missing RIFF magic",
120            });
121        }
122
123        if data[8..12] != WAVE_MAGIC {
124            return Err(KernelError::InvalidArgument {
125                name: "wav_data",
126                value: "missing WAVE magic",
127            });
128        }
129
130        // Search for fmt and data chunks
131        let mut pos = 12;
132        let mut fmt_found = false;
133        let mut num_channels: u16 = 0;
134        let mut sample_rate: u32 = 0;
135        let mut bits_per_sample: u16 = 0;
136        let mut audio_format: u16 = 0;
137        let mut block_align: u16 = 0;
138        let mut data_offset: usize = 0;
139        let mut data_size: usize = 0;
140        let mut data_found = false;
141
142        while pos + 8 <= data.len() {
143            let chunk_id = [data[pos], data[pos + 1], data[pos + 2], data[pos + 3]];
144            let chunk_size = read_u32_le(&data[pos + 4..pos + 8]) as usize;
145
146            if chunk_id == FMT_CHUNK_ID {
147                if pos + 8 + 16 > data.len() {
148                    return Err(KernelError::InvalidArgument {
149                        name: "wav_data",
150                        value: "fmt chunk truncated",
151                    });
152                }
153
154                let fmt_data = &data[pos + 8..];
155                audio_format = read_u16_le(&fmt_data[0..2]);
156                num_channels = read_u16_le(&fmt_data[2..4]);
157                sample_rate = read_u32_le(&fmt_data[4..8]);
158                // byte_rate at [8..12]
159                block_align = read_u16_le(&fmt_data[12..14]);
160                bits_per_sample = read_u16_le(&fmt_data[14..16]);
161
162                if audio_format != AUDIO_FORMAT_PCM {
163                    return Err(KernelError::InvalidArgument {
164                        name: "audio_format",
165                        value: "only PCM format supported",
166                    });
167                }
168
169                fmt_found = true;
170            } else if chunk_id == DATA_CHUNK_ID {
171                data_offset = pos + 8;
172                data_size = chunk_size.min(data.len() - data_offset);
173                data_found = true;
174            }
175
176            // Move to next chunk (chunks are word-aligned)
177            let padded_size = (chunk_size + 1) & !1;
178            pos += 8 + padded_size;
179        }
180
181        if !fmt_found {
182            return Err(KernelError::InvalidArgument {
183                name: "wav_data",
184                value: "missing fmt chunk",
185            });
186        }
187
188        if !data_found {
189            return Err(KernelError::InvalidArgument {
190                name: "wav_data",
191                value: "missing data chunk",
192            });
193        }
194
195        Ok(WavFile {
196            num_channels,
197            sample_rate,
198            bits_per_sample,
199            audio_format,
200            data_offset,
201            data_size,
202            block_align,
203        })
204    }
205
206    /// Get a reference to the PCM data within the source buffer
207    pub fn sample_data<'a>(&self, source: &'a [u8]) -> &'a [u8] {
208        let end = (self.data_offset + self.data_size).min(source.len());
209        &source[self.data_offset..end]
210    }
211
212    /// Convert this WAV file's format info to an `AudioConfig`
213    pub fn to_audio_config(&self) -> AudioConfig {
214        let format = match self.bits_per_sample {
215            8 => SampleFormat::U8,
216            16 => SampleFormat::S16Le,
217            24 => SampleFormat::S24Le,
218            32 => SampleFormat::S32Le,
219            _ => SampleFormat::S16Le, // fallback
220        };
221
222        AudioConfig {
223            sample_rate: self.sample_rate,
224            channels: self.num_channels as u8,
225            format,
226            buffer_frames: 1024,
227        }
228    }
229
230    /// Calculate the duration of the audio in milliseconds
231    pub fn duration_ms(&self) -> u64 {
232        if self.sample_rate == 0 || self.block_align == 0 {
233            return 0;
234        }
235        let total_frames = self.data_size as u64 / self.block_align as u64;
236        (total_frames * 1000) / self.sample_rate as u64
237    }
238
239    /// Calculate the total number of samples (all channels)
240    pub fn total_samples(&self) -> usize {
241        if self.bits_per_sample == 0 {
242            return 0;
243        }
244        let bytes_per_sample = self.bits_per_sample as usize / 8;
245        if bytes_per_sample == 0 {
246            return 0;
247        }
248        self.data_size / bytes_per_sample
249    }
250}
251
252// ============================================================================
253// Format Conversion Utilities
254// ============================================================================
255
256/// Convert unsigned 8-bit PCM to signed 16-bit PCM
257///
258/// U8 samples are centered at 128 (silence). This function shifts to signed
259/// and scales to 16-bit range.
260pub fn convert_u8_to_s16(data: &[u8]) -> Vec<i16> {
261    let mut output = Vec::with_capacity(data.len());
262    for &sample in data {
263        // U8 is unsigned 0..255 centered at 128
264        // Convert to signed: subtract 128, then scale to 16-bit
265        let signed = (sample as i16) - 128;
266        output.push(signed << 8); // Scale 8-bit to 16-bit range
267    }
268    output
269}
270
271/// Convert signed 24-bit little-endian PCM to signed 16-bit PCM
272///
273/// Reads packed 3-byte samples and truncates to 16-bit by discarding the
274/// least significant 8 bits.
275pub fn convert_s24_to_s16(data: &[u8]) -> Vec<i16> {
276    let num_samples = data.len() / 3;
277    let mut output = Vec::with_capacity(num_samples);
278
279    for i in 0..num_samples {
280        let offset = i * 3;
281        if offset + 2 >= data.len() {
282            break;
283        }
284
285        // 24-bit LE: [low, mid, high]
286        // Reconstruct as i32, then sign-extend from 24 bits
287        let low = data[offset] as i32;
288        let mid = data[offset + 1] as i32;
289        let high = data[offset + 2] as i32;
290        let sample_24 = low | (mid << 8) | (high << 16);
291
292        // Sign-extend from 24 bits
293        let signed = if sample_24 & 0x800000 != 0 {
294            sample_24 | !0xFFFFFF_u32 as i32
295        } else {
296            sample_24
297        };
298
299        // Truncate to 16-bit: shift right by 8
300        output.push((signed >> 8) as i16);
301    }
302    output
303}
304
305/// Convert signed 32-bit little-endian PCM to signed 16-bit PCM
306///
307/// Discards the lower 16 bits of each 32-bit sample.
308pub fn convert_s32_to_s16(data: &[u8]) -> Vec<i16> {
309    let num_samples = data.len() / 4;
310    let mut output = Vec::with_capacity(num_samples);
311
312    for i in 0..num_samples {
313        let offset = i * 4;
314        if offset + 3 >= data.len() {
315            break;
316        }
317
318        let sample_32 = read_i32_le(&data[offset..offset + 4]);
319        // Truncate to 16-bit: shift right by 16
320        output.push((sample_32 >> 16) as i16);
321    }
322    output
323}
324
325// ============================================================================
326// Little-Endian Reading Helpers
327// ============================================================================
328
329#[inline]
330fn read_u16_le(data: &[u8]) -> u16 {
331    u16::from_le_bytes([data[0], data[1]])
332}
333
334#[inline]
335fn read_u32_le(data: &[u8]) -> u32 {
336    u32::from_le_bytes([data[0], data[1], data[2], data[3]])
337}
338
339#[inline]
340fn read_i32_le(data: &[u8]) -> i32 {
341    i32::from_le_bytes([data[0], data[1], data[2], data[3]])
342}
343
344// ============================================================================
345// Tests
346// ============================================================================
347
348#[cfg(test)]
349mod tests {
350    use super::*;
351
352    /// Build a minimal valid WAV file in memory
353    fn build_test_wav(
354        channels: u16,
355        sample_rate: u32,
356        bits_per_sample: u16,
357        pcm_data: &[u8],
358    ) -> Vec<u8> {
359        let block_align = channels * (bits_per_sample / 8);
360        let byte_rate = sample_rate * block_align as u32;
361        let data_size = pcm_data.len() as u32;
362        let fmt_chunk_size: u32 = 16;
363        let file_size = 4 + (8 + fmt_chunk_size) + (8 + data_size);
364
365        let mut wav = Vec::new();
366
367        // RIFF header
368        wav.extend_from_slice(b"RIFF");
369        wav.extend_from_slice(&file_size.to_le_bytes());
370        wav.extend_from_slice(b"WAVE");
371
372        // fmt chunk
373        wav.extend_from_slice(b"fmt ");
374        wav.extend_from_slice(&fmt_chunk_size.to_le_bytes());
375        wav.extend_from_slice(&1u16.to_le_bytes()); // PCM format
376        wav.extend_from_slice(&channels.to_le_bytes());
377        wav.extend_from_slice(&sample_rate.to_le_bytes());
378        wav.extend_from_slice(&byte_rate.to_le_bytes());
379        wav.extend_from_slice(&block_align.to_le_bytes());
380        wav.extend_from_slice(&bits_per_sample.to_le_bytes());
381
382        // data chunk
383        wav.extend_from_slice(b"data");
384        wav.extend_from_slice(&data_size.to_le_bytes());
385        wav.extend_from_slice(pcm_data);
386
387        wav
388    }
389
390    #[test]
391    fn test_parse_valid_wav() {
392        let pcm = [0u8; 100]; // 50 samples of 16-bit mono
393        let wav_data = build_test_wav(1, 44100, 16, &pcm);
394
395        let wav = WavFile::parse(&wav_data).unwrap();
396        assert_eq!(wav.num_channels, 1);
397        assert_eq!(wav.sample_rate, 44100);
398        assert_eq!(wav.bits_per_sample, 16);
399        assert_eq!(wav.audio_format, 1);
400        assert_eq!(wav.data_size, 100);
401    }
402
403    #[test]
404    fn test_parse_stereo_wav() {
405        let pcm = [0u8; 200]; // 50 frames of 16-bit stereo
406        let wav_data = build_test_wav(2, 48000, 16, &pcm);
407
408        let wav = WavFile::parse(&wav_data).unwrap();
409        assert_eq!(wav.num_channels, 2);
410        assert_eq!(wav.sample_rate, 48000);
411        assert_eq!(wav.total_samples(), 100); // 200 bytes / 2 bytes per sample
412    }
413
414    #[test]
415    fn test_parse_invalid_magic() {
416        let mut wav_data = build_test_wav(1, 44100, 16, &[0u8; 10]);
417        wav_data[0] = b'X'; // Corrupt RIFF magic
418
419        let result = WavFile::parse(&wav_data);
420        assert!(result.is_err());
421    }
422
423    #[test]
424    fn test_parse_invalid_wave_magic() {
425        let mut wav_data = build_test_wav(1, 44100, 16, &[0u8; 10]);
426        wav_data[8] = b'X'; // Corrupt WAVE magic
427
428        let result = WavFile::parse(&wav_data);
429        assert!(result.is_err());
430    }
431
432    #[test]
433    fn test_parse_too_small() {
434        let data = [0u8; 20]; // Too small for a WAV header
435        let result = WavFile::parse(&data);
436        assert!(result.is_err());
437    }
438
439    #[test]
440    fn test_sample_data() {
441        let pcm = [1u8, 2, 3, 4, 5, 6, 7, 8];
442        let wav_data = build_test_wav(1, 44100, 16, &pcm);
443        let wav = WavFile::parse(&wav_data).unwrap();
444        let samples = wav.sample_data(&wav_data);
445        assert_eq!(samples, &pcm);
446    }
447
448    #[test]
449    fn test_to_audio_config() {
450        let pcm = [0u8; 16];
451        let wav_data = build_test_wav(2, 48000, 16, &pcm);
452        let wav = WavFile::parse(&wav_data).unwrap();
453        let config = wav.to_audio_config();
454        assert_eq!(config.sample_rate, 48000);
455        assert_eq!(config.channels, 2);
456        assert_eq!(config.format, SampleFormat::S16Le);
457    }
458
459    #[test]
460    fn test_duration_ms() {
461        // 44100 Hz, mono, 16-bit: 2 bytes per frame
462        // 88200 bytes = 44100 frames = 1000ms
463        let pcm = [0u8; 88200];
464        let wav_data = build_test_wav(1, 44100, 16, &pcm);
465        let wav = WavFile::parse(&wav_data).unwrap();
466        assert_eq!(wav.duration_ms(), 1000);
467    }
468
469    #[test]
470    fn test_convert_u8_to_s16() {
471        let data = [128u8, 0, 255]; // Silence, min, max
472        let result = convert_u8_to_s16(&data);
473        assert_eq!(result[0], 0); // 128 -> 0
474        assert_eq!(result[1], -128 << 8); // 0 -> -128*256 = -32768
475        assert_eq!(result[2], 127 << 8); // 255 -> 127*256 = 32512
476    }
477
478    #[test]
479    fn test_convert_s24_to_s16() {
480        // 24-bit sample: 0x007FFF (positive max / 256)
481        let data = [0xFF, 0x7F, 0x00]; // LE: low=0xFF, mid=0x7F, high=0x00
482        let result = convert_s24_to_s16(&data);
483        assert_eq!(result[0], 0x7F); // 0x007FFF >> 8 = 0x7F
484    }
485
486    #[test]
487    fn test_total_samples() {
488        let pcm = [0u8; 40]; // 40 bytes
489        let wav_data = build_test_wav(2, 44100, 16, &pcm);
490        let wav = WavFile::parse(&wav_data).unwrap();
491        // 40 bytes / 2 bytes per sample = 20 samples
492        assert_eq!(wav.total_samples(), 20);
493    }
494}