// WAV file format validator
///|
/// Validation result with detailed error information.
pub struct ValidationResult {
/// Whether the file is valid
is_valid : Bool
/// List of warnings (non-fatal issues)
warnings : Array[String]
/// Error message if invalid
error : String?
} derive(Debug)
///|
/// Validate a WAV file and return detailed results.
///
/// Checks:
/// - RIFF header correctness
/// - WAVE marker presence
/// - fmt chunk structure and values
/// - data chunk presence
/// - Chunk alignment
/// - Data size consistency
///
/// # Arguments
/// * `data` - The raw bytes of the WAV file
///
/// # Returns
/// * `ValidationResult` - Detailed validation result
pub fn validate(data : Bytes) -> ValidationResult {
let warnings = []
// Check minimum file size (RIFF header = 12 bytes)
if data.length() < 12 {
return { is_valid: false, warnings, error: Some("File too small: minimum 12 bytes required") }
}
// Check RIFF header
match data {
[b'R', b'I', b'F', b'F', _, _, _, _, b'W', b'A', b'V', b'E', ..] => ()
_ => return { is_valid: false, warnings, error: Some("Invalid RIFF/WAVE header") }
}
// Parse RIFF size
let riff_size = parse_le_u32(data, 4)
if riff_size + 8 != data.length() {
warnings.push("RIFF size field does not match file size")
}
// Parse chunks
let view = data[12:]
let mut pos = 0
let mut has_fmt = false
let mut has_data = false
let mut fmt_audio_format = 0
let mut fmt_num_channels = 0
let mut fmt_sample_rate = 0
let mut fmt_bits_per_sample = 0
let mut data_chunk_size = 0
while pos + 8 <= view.length() {
let chunk_id = [view[pos], view[pos + 1], view[pos + 2], view[pos + 3]]
let chunk_size = parse_le_u32_from_view(view, pos + 4)
if chunk_id == [b'f', b'm', b't', b' '] {
has_fmt = true
if chunk_size < 16 {
return { is_valid: false, warnings, error: Some("fmt chunk too small: minimum 16 bytes") }
}
if pos + 8 + chunk_size > view.length() {
return { is_valid: false, warnings, error: Some("fmt chunk extends beyond file") }
}
fmt_audio_format = parse_le_u16_from_view(view, pos + 8)
fmt_num_channels = parse_le_u16_from_view(view, pos + 10)
fmt_sample_rate = parse_le_u32_from_view(view, pos + 12)
fmt_bits_per_sample = parse_le_u16_from_view(view, pos + 22)
// Validate format values
if fmt_audio_format != 1 && fmt_audio_format != 3 {
warnings.push("Audio format is not PCM (1) or IEEE float (3)")
}
if fmt_num_channels < 1 {
return { is_valid: false, warnings, error: Some("Invalid channel count: must be >= 1") }
}
if fmt_num_channels > 2 {
warnings.push("More than 2 channels detected; only mono and stereo are fully supported")
}
if fmt_sample_rate < 1 {
return { is_valid: false, warnings, error: Some("Invalid sample rate: must be >= 1") }
}
if fmt_sample_rate > 192000 {
warnings.push("Sample rate exceeds 192000 Hz; this is unusual")
}
if fmt_audio_format == 1 {
if fmt_bits_per_sample != 8 && fmt_bits_per_sample != 16 &&
fmt_bits_per_sample != 24 && fmt_bits_per_sample != 32 {
return { is_valid: false, warnings, error: Some("Unsupported PCM bit depth: must be 8, 16, 24, or 32") }
}
}
if fmt_audio_format == 3 && fmt_bits_per_sample != 32 {
return { is_valid: false, warnings, error: Some("IEEE float format requires 32 bits per sample") }
}
} else if chunk_id == [b'd', b'a', b't', b'a'] {
has_data = true
data_chunk_size = chunk_size
if pos + 8 + chunk_size > view.length() {
warnings.push("Data chunk extends beyond file; file may be truncated")
}
}
// Move to next chunk (2-byte aligned)
pos = pos + 8 + chunk_size
if chunk_size % 2 != 0 {
pos = pos + 1
}
}
// Check required chunks
if !has_fmt {
return { is_valid: false, warnings, error: Some("Missing fmt chunk") }
}
if !has_data {
return { is_valid: false, warnings, error: Some("Missing data chunk") }
}
// Validate data size vs format
if has_fmt && has_data {
let bytes_per_sample = fmt_bits_per_sample / 8
let expected_frame_size = fmt_num_channels * bytes_per_sample
if expected_frame_size > 0 && data_chunk_size % expected_frame_size != 0 {
warnings.push("Data chunk size is not a multiple of frame size")
}
}
{ is_valid: true, warnings, error: None }
}
///|
/// Parse a little-endian 16-bit unsigned integer from BytesView.
fn parse_le_u16_from_view(view : BytesView, pos : Int) -> Int {
view[pos].to_int() | (view[pos + 1].to_int() << 8)
}
///|
/// Parse a little-endian 32-bit unsigned integer from BytesView.
fn parse_le_u32_from_view(view : BytesView, pos : Int) -> Int {
view[pos].to_int() |
(view[pos + 1].to_int() << 8) |
(view[pos + 2].to_int() << 16) |
(view[pos + 3].to_int() << 24)
}
///|
/// Parse a little-endian 32-bit unsigned integer from Bytes.
fn parse_le_u32(data : Bytes, offset : Int) -> Int {
let view = data[:]
view[offset].to_int() |
(view[offset + 1].to_int() << 8) |
(view[offset + 2].to_int() << 16) |
(view[offset + 3].to_int() << 24)
}