use oximedia_core::{OxiError, OxiResult};
#[derive(Clone, Debug)]
pub struct RiffChunk {
pub id: [u8; 4],
pub size: u32,
}
impl RiffChunk {
pub const HEADER_SIZE: usize = 8;
pub fn parse(data: &[u8]) -> OxiResult<Self> {
if data.len() < Self::HEADER_SIZE {
return Err(OxiError::UnexpectedEof);
}
let mut id = [0u8; 4];
id.copy_from_slice(&data[0..4]);
let size = u32::from_le_bytes([data[4], data[5], data[6], data[7]]);
Ok(Self { id, size })
}
#[must_use]
pub fn id_str(&self) -> &str {
std::str::from_utf8(&self.id).unwrap_or("????")
}
#[must_use]
pub fn is(&self, id: &[u8; 4]) -> bool {
&self.id == id
}
#[must_use]
pub fn total_size(&self) -> u64 {
u64::from(self.size) + Self::HEADER_SIZE as u64
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum WavFormat {
Pcm,
IeeeFloat,
Alaw,
Mulaw,
Extensible,
Unknown(u16),
}
impl From<u16> for WavFormat {
fn from(value: u16) -> Self {
match value {
0x0001 => Self::Pcm,
0x0003 => Self::IeeeFloat,
0x0006 => Self::Alaw,
0x0007 => Self::Mulaw,
0xFFFE => Self::Extensible,
other => Self::Unknown(other),
}
}
}
impl WavFormat {
#[must_use]
pub fn is_pcm(&self) -> bool {
matches!(self, Self::Pcm | Self::IeeeFloat)
}
#[must_use]
pub fn code(&self) -> u16 {
match self {
Self::Pcm => 0x0001,
Self::IeeeFloat => 0x0003,
Self::Alaw => 0x0006,
Self::Mulaw => 0x0007,
Self::Extensible => 0xFFFE,
Self::Unknown(code) => *code,
}
}
}
#[derive(Clone, Debug)]
pub struct FmtChunk {
pub format: WavFormat,
pub channels: u16,
pub sample_rate: u32,
pub byte_rate: u32,
pub block_align: u16,
pub bits_per_sample: u16,
pub extension: Option<FmtExtension>,
}
#[derive(Clone, Debug)]
pub struct FmtExtension {
pub valid_bits: u16,
pub channel_mask: u32,
pub sub_format: [u8; 16],
}
impl FmtChunk {
pub const MIN_SIZE: usize = 16;
pub const EXTENDED_SIZE: usize = 40;
pub fn parse(data: &[u8]) -> OxiResult<Self> {
if data.len() < Self::MIN_SIZE {
return Err(OxiError::Parse {
offset: 0,
message: "fmt chunk too short".into(),
});
}
let format = WavFormat::from(u16::from_le_bytes([data[0], data[1]]));
let channels = u16::from_le_bytes([data[2], data[3]]);
let sample_rate = u32::from_le_bytes([data[4], data[5], data[6], data[7]]);
let byte_rate = u32::from_le_bytes([data[8], data[9], data[10], data[11]]);
let block_align = u16::from_le_bytes([data[12], data[13]]);
let bits_per_sample = u16::from_le_bytes([data[14], data[15]]);
let extension = if format == WavFormat::Extensible && data.len() >= Self::EXTENDED_SIZE {
let cb_size = u16::from_le_bytes([data[16], data[17]]);
if cb_size >= 22 {
let valid_bits = u16::from_le_bytes([data[18], data[19]]);
let channel_mask = u32::from_le_bytes([data[20], data[21], data[22], data[23]]);
let mut sub_format = [0u8; 16];
sub_format.copy_from_slice(&data[24..40]);
Some(FmtExtension {
valid_bits,
channel_mask,
sub_format,
})
} else {
None
}
} else {
None
};
Ok(Self {
format,
channels,
sample_rate,
byte_rate,
block_align,
bits_per_sample,
extension,
})
}
#[must_use]
pub fn actual_bits_per_sample(&self) -> u16 {
self.extension
.as_ref()
.map_or(self.bits_per_sample, |ext| ext.valid_bits)
}
#[must_use]
pub fn is_float(&self) -> bool {
match &self.format {
WavFormat::IeeeFloat => true,
WavFormat::Extensible => {
if let Some(ext) = &self.extension {
ext.sub_format[0..2] == [0x03, 0x00]
} else {
false
}
}
_ => false,
}
}
#[must_use]
pub fn is_integer_pcm(&self) -> bool {
match &self.format {
WavFormat::Pcm => true,
WavFormat::Extensible => {
if let Some(ext) = &self.extension {
ext.sub_format[0..2] == [0x01, 0x00]
} else {
false
}
}
_ => false,
}
}
}
#[allow(dead_code)]
pub mod channel_mask {
pub const FRONT_LEFT: u32 = 0x0000_0001;
pub const FRONT_RIGHT: u32 = 0x0000_0002;
pub const FRONT_CENTER: u32 = 0x0000_0004;
pub const LOW_FREQUENCY: u32 = 0x0000_0008;
pub const BACK_LEFT: u32 = 0x0000_0010;
pub const BACK_RIGHT: u32 = 0x0000_0020;
pub const FRONT_LEFT_OF_CENTER: u32 = 0x0000_0040;
pub const FRONT_RIGHT_OF_CENTER: u32 = 0x0000_0080;
pub const BACK_CENTER: u32 = 0x0000_0100;
pub const SIDE_LEFT: u32 = 0x0000_0200;
pub const SIDE_RIGHT: u32 = 0x0000_0400;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_riff_chunk_parse() {
let data = b"RIFF\x10\x00\x00\x00WAVE";
let chunk = RiffChunk::parse(data).expect("operation should succeed");
assert_eq!(chunk.id_str(), "RIFF");
assert_eq!(chunk.size, 16);
assert!(chunk.is(b"RIFF"));
assert!(!chunk.is(b"WAVE"));
}
#[test]
fn test_riff_chunk_too_short() {
let data = b"RIFF";
assert!(RiffChunk::parse(data).is_err());
}
#[test]
fn test_riff_chunk_total_size() {
let data = b"fmt \x10\x00\x00\x00";
let chunk = RiffChunk::parse(data).expect("operation should succeed");
assert_eq!(chunk.total_size(), 24);
}
#[test]
fn test_fmt_chunk_pcm_16bit_stereo() {
let data = [
0x01, 0x00, 0x02, 0x00, 0x44, 0xAC, 0x00, 0x00, 0x10, 0xB1, 0x02, 0x00, 0x04, 0x00, 0x10, 0x00, ];
let fmt = FmtChunk::parse(&data).expect("operation should succeed");
assert_eq!(fmt.format, WavFormat::Pcm);
assert_eq!(fmt.channels, 2);
assert_eq!(fmt.sample_rate, 44_100);
assert_eq!(fmt.byte_rate, 176_400);
assert_eq!(fmt.block_align, 4);
assert_eq!(fmt.bits_per_sample, 16);
assert!(fmt.is_integer_pcm());
assert!(!fmt.is_float());
}
#[test]
fn test_fmt_chunk_float_mono() {
let data = [
0x03, 0x00, 0x01, 0x00, 0x80, 0xBB, 0x00, 0x00, 0x00, 0xEE, 0x02, 0x00, 0x04, 0x00, 0x20, 0x00, ];
let fmt = FmtChunk::parse(&data).expect("operation should succeed");
assert_eq!(fmt.format, WavFormat::IeeeFloat);
assert_eq!(fmt.channels, 1);
assert_eq!(fmt.sample_rate, 48_000);
assert!(fmt.is_float());
assert!(!fmt.is_integer_pcm());
}
#[test]
fn test_fmt_chunk_too_short() {
let data = [0x01, 0x00, 0x02, 0x00];
assert!(FmtChunk::parse(&data).is_err());
}
#[test]
fn test_wav_format_conversion() {
assert_eq!(WavFormat::from(1), WavFormat::Pcm);
assert_eq!(WavFormat::from(3), WavFormat::IeeeFloat);
assert_eq!(WavFormat::from(6), WavFormat::Alaw);
assert_eq!(WavFormat::from(7), WavFormat::Mulaw);
assert_eq!(WavFormat::from(0xFFFE), WavFormat::Extensible);
assert!(matches!(WavFormat::from(99), WavFormat::Unknown(99)));
}
#[test]
fn test_wav_format_is_pcm() {
assert!(WavFormat::Pcm.is_pcm());
assert!(WavFormat::IeeeFloat.is_pcm());
assert!(!WavFormat::Alaw.is_pcm());
assert!(!WavFormat::Mulaw.is_pcm());
}
#[test]
fn test_wav_format_code() {
assert_eq!(WavFormat::Pcm.code(), 0x0001);
assert_eq!(WavFormat::IeeeFloat.code(), 0x0003);
assert_eq!(WavFormat::Unknown(0x55).code(), 0x55);
}
#[test]
fn test_actual_bits_per_sample() {
let data = [
0x01, 0x00, 0x02, 0x00, 0x44, 0xAC, 0x00, 0x00, 0x10, 0xB1, 0x02, 0x00, 0x04, 0x00,
0x18, 0x00, ];
let fmt = FmtChunk::parse(&data).expect("operation should succeed");
assert_eq!(fmt.actual_bits_per_sample(), 24);
}
}