use crate::error::{Error, ParseError};
pub fn mono_pcm16(samples: &[i16], rate: u32) -> Result<Vec<u8>, Error> {
pcm16(samples, rate, 1)
}
pub fn pcm16(samples: &[i16], rate: u32, channels: u16) -> Result<Vec<u8>, Error> {
const BITS: u16 = 16;
let block = channels
.checked_mul(BITS / 8)
.filter(|&bytes| bytes > 0)
.ok_or_else(|| ParseError::OutOfBounds {
value: format!("{channels} channels"),
bound: "a positive channel count whose frame size fits u16".into(),
})?;
if rate == 0 {
return Err(ParseError::OutOfBounds {
value: "0 Hz".into(),
bound: "a positive sample rate".into(),
}
.into());
}
if !samples.len().is_multiple_of(usize::from(channels)) {
return Err(ParseError::OutOfBounds {
value: format!("{} samples", samples.len()),
bound: format!("whole {channels}-channel frames"),
}
.into());
}
let data = samples
.len()
.checked_mul(usize::from(BITS / 8))
.and_then(|n| u32::try_from(n).ok())
.filter(|&n| n <= u32::MAX - 36)
.ok_or_else(|| ParseError::OutOfBounds {
value: format!("{} samples", samples.len()),
bound: "a WAV whose RIFF and data lengths fit u32".into(),
})?;
let byte_rate = rate
.checked_mul(u32::from(block))
.ok_or_else(|| ParseError::OutOfBounds {
value: format!("{rate} Hz"),
bound: "a WAV byte rate that fits u32".into(),
})?;
let mut out = Vec::new();
out.try_reserve(44 + data as usize)
.map_err(|_| ParseError::OutOfBounds {
value: format!("{} samples", samples.len()),
bound: "a WAV whose allocation fits memory".into(),
})?;
out.extend_from_slice(b"RIFF");
out.extend_from_slice(&(36 + data).to_le_bytes());
out.extend_from_slice(b"WAVEfmt ");
out.extend_from_slice(&16u32.to_le_bytes()); out.extend_from_slice(&1u16.to_le_bytes()); out.extend_from_slice(&channels.to_le_bytes());
out.extend_from_slice(&rate.to_le_bytes());
out.extend_from_slice(&byte_rate.to_le_bytes());
out.extend_from_slice(&block.to_le_bytes());
out.extend_from_slice(&BITS.to_le_bytes());
out.extend_from_slice(b"data");
out.extend_from_slice(&data.to_le_bytes());
for s in samples {
out.extend_from_slice(&s.to_le_bytes());
}
Ok(out)
}
#[derive(Debug)]
pub struct Pcm16 {
pub rate: u32,
pub channels: u16,
pub samples: Vec<i16>,
}
impl Pcm16 {
pub fn frames(&self) -> usize {
self.samples.len() / usize::from(self.channels).max(1)
}
}
pub fn read_pcm16(bytes: &[u8]) -> Result<Pcm16, Error> {
let u16_at = |at: usize| u16::from_le_bytes([bytes[at], bytes[at + 1]]);
let u32_at =
|at: usize| u32::from_le_bytes([bytes[at], bytes[at + 1], bytes[at + 2], bytes[at + 3]]);
if bytes.len() < 12 || &bytes[..4] != b"RIFF" || &bytes[8..12] != b"WAVE" {
return Err(ParseError::AssertFail("not a RIFF/WAVE file".into()).into());
}
let declared = u32_at(4) as usize;
let riff_len = declared
.checked_add(8)
.filter(|&n| n == bytes.len())
.ok_or_else(|| {
ParseError::AssertFail(format!(
"RIFF declares {declared} payload bytes but the file is {} bytes",
bytes.len()
))
})?;
let bytes = &bytes[..riff_len];
let mut format = None;
let mut data = None;
let mut at = 12;
while bytes.len() - at >= 8 {
let id = &bytes[at..at + 4];
let size = u32_at(at + 4) as usize;
let body = at + 8;
let end = body.checked_add(size).filter(|&e| e <= bytes.len());
let Some(end) = end else {
return Err(ParseError::AssertFail(format!(
"chunk {} claims {size} bytes but the file ends first",
String::from_utf8_lossy(id)
))
.into());
};
match id {
b"fmt " if size >= 16 => {
format = Some((
u16_at(body),
u16_at(body + 2),
u32_at(body + 4),
u32_at(body + 8),
u16_at(body + 12),
u16_at(body + 14),
))
}
b"fmt " => {
return Err(ParseError::AssertFail(format!(
"fmt chunk is {size} bytes; PCM requires at least 16"
))
.into())
}
b"data" => data = Some(body..end),
_ => {}
}
at = end
.checked_add(size % 2)
.filter(|&next| next <= bytes.len())
.ok_or_else(|| ParseError::AssertFail("an odd-sized chunk has no pad byte".into()))?;
}
if at != bytes.len() {
return Err(ParseError::AssertFail(format!(
"{} trailing byte(s) do not form a chunk",
bytes.len() - at
))
.into());
}
let Some((encoding, channels, rate, byte_rate, block, bits)) = format else {
return Err(ParseError::AssertFail("no fmt chunk".into()).into());
};
if encoding != 1 {
return Err(ParseError::AssertFail(format!(
"encoding {encoding} is not uncompressed PCM; only PCM (1) is read"
))
.into());
}
if bits != 16 {
return Err(
ParseError::AssertFail(format!("{bits}-bit samples; only 16-bit PCM is read")).into(),
);
}
if channels == 0 {
return Err(ParseError::AssertFail("the fmt chunk declares no channels".into()).into());
}
if rate == 0 {
return Err(
ParseError::AssertFail("the fmt chunk declares a zero sample rate".into()).into(),
);
}
let expected_block = channels
.checked_mul(bits / 8)
.ok_or_else(|| ParseError::AssertFail("the channel frame size overflows u16".into()))?;
let expected_rate = rate
.checked_mul(u32::from(expected_block))
.ok_or_else(|| ParseError::AssertFail("the byte rate overflows u32".into()))?;
if block != expected_block || byte_rate != expected_rate {
return Err(ParseError::AssertFail(format!(
"fmt declares byte rate {byte_rate} and block size {block}; expected {expected_rate} and {expected_block}"
))
.into());
}
let Some(data) = data else {
return Err(ParseError::AssertFail("no data chunk".into()).into());
};
if data.len() % usize::from(expected_block) != 0 {
return Err(ParseError::AssertFail(format!(
"data chunk is {} bytes, not a whole {expected_block}-byte frame",
data.len()
))
.into());
}
let samples = bytes[data]
.chunks_exact(2)
.map(|s| i16::from_le_bytes([s[0], s[1]]))
.collect();
Ok(Pcm16 {
rate,
channels,
samples,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn what_this_writes_it_reads_back() {
let want = [0i16, 1, -1, i16::MIN, 12_345];
let read = read_pcm16(&mono_pcm16(&want, 44_100).unwrap()).unwrap();
assert_eq!(read.rate, 44_100);
assert_eq!(read.channels, 1);
assert_eq!(read.samples, want);
assert_eq!(read.frames(), 5);
let stereo = [1, -1, 2, -2];
let read = read_pcm16(&pcm16(&stereo, 35_002, 2).unwrap()).unwrap();
assert_eq!((read.rate, read.channels, read.frames()), (35_002, 2, 2));
assert_eq!(read.samples, stereo);
}
#[test]
fn a_chunk_before_the_data_is_walked_past() {
let mut wav = mono_pcm16(&[7i16, 8], 44_100).unwrap();
let extra: Vec<u8> = b"LIST\x03\x00\x00\x00abc\x00".to_vec();
wav.splice(36..36, extra.iter().copied());
let size = u32::from_le_bytes(wav[4..8].try_into().unwrap()) + extra.len() as u32;
wav[4..8].copy_from_slice(&size.to_le_bytes());
assert_eq!(read_pcm16(&wav).unwrap().samples, vec![7, 8]);
}
#[test]
fn anything_but_sixteen_bit_pcm_is_refused_by_name() {
assert!(read_pcm16(b"not a wav at all").is_err());
let mut wav = mono_pcm16(&[1i16], 44_100).unwrap();
wav[34] = 24; assert!(read_pcm16(&wav).unwrap_err().to_string().contains("24-bit"));
let mut wav = mono_pcm16(&[1i16], 44_100).unwrap();
wav[20] = 3; assert!(read_pcm16(&wav).unwrap_err().to_string().contains("PCM"));
let mut wav = mono_pcm16(&[1i16], 44_100).unwrap();
wav[40..44].copy_from_slice(&999u32.to_le_bytes()); assert!(read_pcm16(&wav).is_err());
}
#[test]
fn malformed_rates_and_frames_are_refused() {
assert!(mono_pcm16(&[], 0).is_err());
assert!(pcm16(&[], 44_100, 0).is_err());
assert!(pcm16(&[1], 44_100, 2).is_err());
let mut zero_rate = mono_pcm16(&[1], 44_100).unwrap();
zero_rate[24..28].fill(0);
zero_rate[28..32].fill(0);
assert!(read_pcm16(&zero_rate).is_err());
let mut no_pad = mono_pcm16(&[1], 44_100).unwrap();
no_pad.push(0);
no_pad[4..8].copy_from_slice(&39u32.to_le_bytes());
no_pad[40..44].copy_from_slice(&3u32.to_le_bytes());
let err = read_pcm16(&no_pad).unwrap_err().to_string();
assert!(
err.contains("pad byte"),
"refused for the wrong reason: {err}"
);
let mut stereo_half_frame = mono_pcm16(&[1], 44_100).unwrap();
stereo_half_frame[22..24].copy_from_slice(&2u16.to_le_bytes());
stereo_half_frame[28..32].copy_from_slice(&176_400u32.to_le_bytes());
stereo_half_frame[32..34].copy_from_slice(&4u16.to_le_bytes());
assert!(read_pcm16(&stereo_half_frame).is_err());
}
#[test]
fn a_data_chunk_before_the_fmt_chunk_still_reads() {
let wav = mono_pcm16(&[7i16, -8], 44_100).unwrap();
let mut swapped = wav[..12].to_vec();
swapped.extend_from_slice(&wav[36..]); swapped.extend_from_slice(&wav[12..36]); assert_eq!(swapped.len(), wav.len());
assert_eq!(read_pcm16(&swapped).unwrap().samples, vec![7, -8]);
}
#[test]
fn trailing_bytes_that_form_no_chunk_are_refused() {
let mut trailing = mono_pcm16(&[1i16], 44_100).unwrap();
trailing.extend_from_slice(&[0xde, 0xad, 0xbe, 0xef]);
let declared = u32::from_le_bytes(trailing[4..8].try_into().unwrap()) + 4;
trailing[4..8].copy_from_slice(&declared.to_le_bytes());
let err = read_pcm16(&trailing).unwrap_err().to_string();
assert!(
err.contains("trailing"),
"refused for the wrong reason: {err}"
);
}
#[test]
fn the_header_describes_the_samples_that_follow() {
let wav = mono_pcm16(&[0, 1, -1, i16::MIN], 35_002).unwrap();
assert_eq!(&wav[..4], b"RIFF");
assert_eq!(&wav[8..12], b"WAVE");
assert_eq!(&wav[36..40], b"data");
assert_eq!(wav.len(), 44 + 8);
assert_eq!(u32::from_le_bytes(wav[4..8].try_into().unwrap()), 44);
assert_eq!(u32::from_le_bytes(wav[24..28].try_into().unwrap()), 35_002);
assert_eq!(u32::from_le_bytes(wav[40..44].try_into().unwrap()), 8);
assert_eq!(i16::from_le_bytes(wav[48..50].try_into().unwrap()), -1);
}
}