use oxideav_core::{Error, Result};
#[derive(Clone, Debug)]
pub struct BitmapInfoHeader {
pub width: u32,
pub height: u32,
pub planes: u16,
pub bit_count: u16,
pub compression: [u8; 4],
pub size_image: u32,
pub x_pels_per_meter: i32,
pub y_pels_per_meter: i32,
pub clr_used: u32,
pub clr_important: u32,
pub extradata: Vec<u8>,
pub top_down: bool,
}
pub fn parse_bitmap_info_header(buf: &[u8]) -> Result<BitmapInfoHeader> {
if buf.len() < 40 {
return Err(Error::invalid("AVI: BITMAPINFOHEADER too short"));
}
let _bi_size = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
let width = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]) as i32 as u32;
let height_signed = i32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]);
let top_down = height_signed.is_negative();
let height = height_signed.unsigned_abs();
let planes = u16::from_le_bytes([buf[12], buf[13]]);
let bit_count = u16::from_le_bytes([buf[14], buf[15]]);
let mut compression = [0u8; 4];
compression.copy_from_slice(&buf[16..20]);
let size_image = u32::from_le_bytes([buf[20], buf[21], buf[22], buf[23]]);
let x_pels_per_meter = i32::from_le_bytes([buf[24], buf[25], buf[26], buf[27]]);
let y_pels_per_meter = i32::from_le_bytes([buf[28], buf[29], buf[30], buf[31]]);
let clr_used = u32::from_le_bytes([buf[32], buf[33], buf[34], buf[35]]);
let clr_important = u32::from_le_bytes([buf[36], buf[37], buf[38], buf[39]]);
let extradata = if buf.len() > 40 {
buf[40..].to_vec()
} else {
Vec::new()
};
Ok(BitmapInfoHeader {
width,
height,
planes,
bit_count,
compression,
size_image,
x_pels_per_meter,
y_pels_per_meter,
clr_used,
clr_important,
extradata,
top_down,
})
}
pub const BI_BITFIELDS: [u8; 4] = [3, 0, 0, 0];
pub fn parse_bitfields_masks(extradata: &[u8]) -> Option<(u32, u32, u32)> {
if extradata.len() < 12 {
return None;
}
let r = u32::from_le_bytes([extradata[0], extradata[1], extradata[2], extradata[3]]);
let g = u32::from_le_bytes([extradata[4], extradata[5], extradata[6], extradata[7]]);
let b = u32::from_le_bytes([extradata[8], extradata[9], extradata[10], extradata[11]]);
Some((r, g, b))
}
pub fn write_bitmap_info_header(
width: u32,
height: u32,
compression: [u8; 4],
bit_count: u16,
extradata: &[u8],
) -> Vec<u8> {
write_bitmap_info_header_oriented(width, height, compression, bit_count, extradata, false)
}
pub fn write_bitmap_info_header_oriented(
width: u32,
height: u32,
compression: [u8; 4],
bit_count: u16,
extradata: &[u8],
top_down: bool,
) -> Vec<u8> {
let bi_size = 40u32 + extradata.len() as u32;
let mut out = Vec::with_capacity(bi_size as usize);
out.extend_from_slice(&bi_size.to_le_bytes());
out.extend_from_slice(&width.to_le_bytes());
let height_field: i32 = if top_down {
-(height as i32)
} else {
height as i32
};
out.extend_from_slice(&height_field.to_le_bytes());
out.extend_from_slice(&1u16.to_le_bytes()); out.extend_from_slice(&bit_count.to_le_bytes());
out.extend_from_slice(&compression);
out.extend_from_slice(&0u32.to_le_bytes());
out.extend_from_slice(&0i32.to_le_bytes()); out.extend_from_slice(&0i32.to_le_bytes()); out.extend_from_slice(&0u32.to_le_bytes()); out.extend_from_slice(&0u32.to_le_bytes()); out.extend_from_slice(extradata);
out
}
#[derive(Clone, Debug)]
pub struct WaveFormatEx {
pub format_tag: u16,
pub channels: u16,
pub samples_per_sec: u32,
pub avg_bytes_per_sec: u32,
pub block_align: u16,
pub bits_per_sample: u16,
pub extradata: Vec<u8>,
}
pub fn parse_waveformatex(buf: &[u8]) -> Result<WaveFormatEx> {
if buf.len() < 14 {
return Err(Error::invalid("AVI: WAVEFORMAT(EX) too short"));
}
let format_tag = u16::from_le_bytes([buf[0], buf[1]]);
let channels = u16::from_le_bytes([buf[2], buf[3]]);
let samples_per_sec = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]);
let avg_bytes_per_sec = u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]);
let block_align = u16::from_le_bytes([buf[12], buf[13]]);
let bits_per_sample = if buf.len() >= 16 {
u16::from_le_bytes([buf[14], buf[15]])
} else {
0
};
let extradata = if buf.len() >= 18 {
let cb_size = u16::from_le_bytes([buf[16], buf[17]]) as usize;
if 18 + cb_size <= buf.len() {
buf[18..18 + cb_size].to_vec()
} else {
buf[18..].to_vec()
}
} else {
Vec::new()
};
Ok(WaveFormatEx {
format_tag,
channels,
samples_per_sec,
avg_bytes_per_sec,
block_align,
bits_per_sample,
extradata,
})
}
#[allow(clippy::too_many_arguments)]
pub fn write_waveformatex(
format_tag: u16,
channels: u16,
samples_per_sec: u32,
avg_bytes_per_sec: u32,
block_align: u16,
bits_per_sample: u16,
extradata: &[u8],
) -> Vec<u8> {
let mut out = Vec::with_capacity(18 + extradata.len());
out.extend_from_slice(&format_tag.to_le_bytes());
out.extend_from_slice(&channels.to_le_bytes());
out.extend_from_slice(&samples_per_sec.to_le_bytes());
out.extend_from_slice(&avg_bytes_per_sec.to_le_bytes());
out.extend_from_slice(&block_align.to_le_bytes());
out.extend_from_slice(&bits_per_sample.to_le_bytes());
out.extend_from_slice(&(extradata.len() as u16).to_le_bytes());
out.extend_from_slice(extradata);
out
}
pub const WAVE_FORMAT_EXTENSIBLE: u16 = 0xFFFE;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Guid(pub [u8; 16]);
impl Guid {
pub const fn from_components(data1: u32, data2: u16, data3: u16, data4: [u8; 8]) -> Self {
let d1 = data1.to_le_bytes();
let d2 = data2.to_le_bytes();
let d3 = data3.to_le_bytes();
let bytes = [
d1[0], d1[1], d1[2], d1[3], d2[0], d2[1], d3[0], d3[1], data4[0], data4[1], data4[2],
data4[3], data4[4], data4[5], data4[6], data4[7],
];
Self(bytes)
}
pub fn from_bytes(buf: &[u8]) -> Option<Self> {
if buf.len() < 16 {
return None;
}
let mut out = [0u8; 16];
out.copy_from_slice(&buf[..16]);
Some(Self(out))
}
pub fn display(&self) -> String {
let d1 = u32::from_le_bytes([self.0[0], self.0[1], self.0[2], self.0[3]]);
let d2 = u16::from_le_bytes([self.0[4], self.0[5]]);
let d3 = u16::from_le_bytes([self.0[6], self.0[7]]);
format!(
"{d1:08x}-{d2:04x}-{d3:04x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
self.0[8],
self.0[9],
self.0[10],
self.0[11],
self.0[12],
self.0[13],
self.0[14],
self.0[15],
)
}
pub fn is_ksdataformat_base(&self) -> bool {
const KS_TAIL: [u8; 12] = [
0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71, ];
self.0[4..16] == KS_TAIL
}
pub fn ksdataformat_tag(&self) -> Option<u16> {
if !self.is_ksdataformat_base() {
return None;
}
let d1 = u32::from_le_bytes([self.0[0], self.0[1], self.0[2], self.0[3]]);
if d1 > u16::MAX as u32 {
return None;
}
Some(d1 as u16)
}
}
pub const KSDATAFORMAT_SUBTYPE_PCM: Guid = Guid::from_components(
0x0000_0001,
0x0000,
0x0010,
[0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71],
);
pub const KSDATAFORMAT_SUBTYPE_IEEE_FLOAT: Guid = Guid::from_components(
0x0000_0003,
0x0000,
0x0010,
[0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71],
);
pub const KSDATAFORMAT_SUBTYPE_DRM: Guid = Guid::from_components(
0x0000_0009,
0x0000,
0x0010,
[0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71],
);
pub const KSDATAFORMAT_SUBTYPE_ALAW: Guid = Guid::from_components(
0x0000_0006,
0x0000,
0x0010,
[0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71],
);
pub const KSDATAFORMAT_SUBTYPE_MULAW: Guid = Guid::from_components(
0x0000_0007,
0x0000,
0x0010,
[0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71],
);
pub const KSDATAFORMAT_SUBTYPE_ADPCM: Guid = Guid::from_components(
0x0000_0002,
0x0000,
0x0010,
[0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71],
);
pub const KSDATAFORMAT_SUBTYPE_MPEG: Guid = Guid::from_components(
0x0000_0050,
0x0000,
0x0010,
[0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71],
);
pub const SPEAKER_FRONT_LEFT: u32 = 0x0000_0001;
pub const SPEAKER_FRONT_RIGHT: u32 = 0x0000_0002;
pub const SPEAKER_FRONT_CENTER: u32 = 0x0000_0004;
pub const SPEAKER_LOW_FREQUENCY: u32 = 0x0000_0008;
pub const SPEAKER_BACK_LEFT: u32 = 0x0000_0010;
pub const SPEAKER_BACK_RIGHT: u32 = 0x0000_0020;
pub const SPEAKER_FRONT_LEFT_OF_CENTER: u32 = 0x0000_0040;
pub const SPEAKER_FRONT_RIGHT_OF_CENTER: u32 = 0x0000_0080;
pub const SPEAKER_BACK_CENTER: u32 = 0x0000_0100;
pub const SPEAKER_SIDE_LEFT: u32 = 0x0000_0200;
pub const SPEAKER_SIDE_RIGHT: u32 = 0x0000_0400;
pub const SPEAKER_TOP_CENTER: u32 = 0x0000_0800;
pub const SPEAKER_TOP_FRONT_LEFT: u32 = 0x0000_1000;
pub const SPEAKER_TOP_FRONT_CENTER: u32 = 0x0000_2000;
pub const SPEAKER_TOP_FRONT_RIGHT: u32 = 0x0000_4000;
pub const SPEAKER_TOP_BACK_LEFT: u32 = 0x0000_8000;
pub const SPEAKER_TOP_BACK_CENTER: u32 = 0x0001_0000;
pub const SPEAKER_TOP_BACK_RIGHT: u32 = 0x0002_0000;
pub const SPEAKER_ALL: u32 = 0x8000_0000;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[non_exhaustive]
pub enum Speaker {
FrontLeft,
FrontRight,
FrontCenter,
LowFrequency,
BackLeft,
BackRight,
FrontLeftOfCenter,
FrontRightOfCenter,
BackCenter,
SideLeft,
SideRight,
TopCenter,
TopFrontLeft,
TopFrontCenter,
TopFrontRight,
TopBackLeft,
TopBackCenter,
TopBackRight,
SpeakerAll,
}
impl Speaker {
pub const fn mask_bit(self) -> u32 {
match self {
Self::FrontLeft => SPEAKER_FRONT_LEFT,
Self::FrontRight => SPEAKER_FRONT_RIGHT,
Self::FrontCenter => SPEAKER_FRONT_CENTER,
Self::LowFrequency => SPEAKER_LOW_FREQUENCY,
Self::BackLeft => SPEAKER_BACK_LEFT,
Self::BackRight => SPEAKER_BACK_RIGHT,
Self::FrontLeftOfCenter => SPEAKER_FRONT_LEFT_OF_CENTER,
Self::FrontRightOfCenter => SPEAKER_FRONT_RIGHT_OF_CENTER,
Self::BackCenter => SPEAKER_BACK_CENTER,
Self::SideLeft => SPEAKER_SIDE_LEFT,
Self::SideRight => SPEAKER_SIDE_RIGHT,
Self::TopCenter => SPEAKER_TOP_CENTER,
Self::TopFrontLeft => SPEAKER_TOP_FRONT_LEFT,
Self::TopFrontCenter => SPEAKER_TOP_FRONT_CENTER,
Self::TopFrontRight => SPEAKER_TOP_FRONT_RIGHT,
Self::TopBackLeft => SPEAKER_TOP_BACK_LEFT,
Self::TopBackCenter => SPEAKER_TOP_BACK_CENTER,
Self::TopBackRight => SPEAKER_TOP_BACK_RIGHT,
Self::SpeakerAll => SPEAKER_ALL,
}
}
pub const fn abbrev(self) -> &'static str {
match self {
Self::FrontLeft => "FL",
Self::FrontRight => "FR",
Self::FrontCenter => "FC",
Self::LowFrequency => "LFE",
Self::BackLeft => "BL",
Self::BackRight => "BR",
Self::FrontLeftOfCenter => "FLC",
Self::FrontRightOfCenter => "FRC",
Self::BackCenter => "BC",
Self::SideLeft => "SL",
Self::SideRight => "SR",
Self::TopCenter => "TC",
Self::TopFrontLeft => "TFL",
Self::TopFrontCenter => "TFC",
Self::TopFrontRight => "TFR",
Self::TopBackLeft => "TBL",
Self::TopBackCenter => "TBC",
Self::TopBackRight => "TBR",
Self::SpeakerAll => "ALL",
}
}
}
const SPEAKER_BIT_ORDER: [Speaker; 19] = [
Speaker::FrontLeft,
Speaker::FrontRight,
Speaker::FrontCenter,
Speaker::LowFrequency,
Speaker::BackLeft,
Speaker::BackRight,
Speaker::FrontLeftOfCenter,
Speaker::FrontRightOfCenter,
Speaker::BackCenter,
Speaker::SideLeft,
Speaker::SideRight,
Speaker::TopCenter,
Speaker::TopFrontLeft,
Speaker::TopFrontCenter,
Speaker::TopFrontRight,
Speaker::TopBackLeft,
Speaker::TopBackCenter,
Speaker::TopBackRight,
Speaker::SpeakerAll,
];
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ChannelMask(pub u32);
impl ChannelMask {
pub const fn from_raw(mask: u32) -> Self {
Self(mask)
}
pub const fn raw(self) -> u32 {
self.0
}
pub fn iter_speakers(self) -> impl Iterator<Item = Speaker> {
let mask = self.0;
SPEAKER_BIT_ORDER
.iter()
.copied()
.filter(move |sp| mask & sp.mask_bit() != 0)
}
pub fn len(self) -> u32 {
let mut count = 0u32;
for sp in SPEAKER_BIT_ORDER {
if self.0 & sp.mask_bit() != 0 {
count += 1;
}
}
count
}
pub fn is_empty(self) -> bool {
self.0 == 0
}
pub fn reserved_bits(self) -> u32 {
let mut known = 0u32;
for sp in SPEAKER_BIT_ORDER {
known |= sp.mask_bit();
}
self.0 & !known
}
pub fn layout(self) -> Option<ChannelLayout> {
ChannelLayout::from_mask(self.0)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ChannelLayout {
Mono,
Stereo,
TwoPointOne,
Quad,
FivePointOneBack,
FivePointOneSide,
SevenPointOne,
}
impl ChannelLayout {
pub const fn mask(self) -> u32 {
match self {
Self::Mono => SPEAKER_FRONT_CENTER,
Self::Stereo => SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT,
Self::TwoPointOne => SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | SPEAKER_LOW_FREQUENCY,
Self::Quad => {
SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT
}
Self::FivePointOneBack => {
SPEAKER_FRONT_LEFT
| SPEAKER_FRONT_RIGHT
| SPEAKER_FRONT_CENTER
| SPEAKER_LOW_FREQUENCY
| SPEAKER_BACK_LEFT
| SPEAKER_BACK_RIGHT
}
Self::FivePointOneSide => {
SPEAKER_FRONT_LEFT
| SPEAKER_FRONT_RIGHT
| SPEAKER_FRONT_CENTER
| SPEAKER_LOW_FREQUENCY
| SPEAKER_SIDE_LEFT
| SPEAKER_SIDE_RIGHT
}
Self::SevenPointOne => {
SPEAKER_FRONT_LEFT
| SPEAKER_FRONT_RIGHT
| SPEAKER_FRONT_CENTER
| SPEAKER_LOW_FREQUENCY
| SPEAKER_BACK_LEFT
| SPEAKER_BACK_RIGHT
| SPEAKER_SIDE_LEFT
| SPEAKER_SIDE_RIGHT
}
}
}
pub fn from_mask(mask: u32) -> Option<Self> {
let mut known = 0u32;
for sp in SPEAKER_BIT_ORDER {
known |= sp.mask_bit();
}
let cleaned = mask & known;
[
Self::Mono,
Self::Stereo,
Self::TwoPointOne,
Self::Quad,
Self::FivePointOneBack,
Self::FivePointOneSide,
Self::SevenPointOne,
]
.into_iter()
.find(|layout| cleaned == layout.mask())
}
pub const fn label(self) -> &'static str {
match self {
Self::Mono => "mono",
Self::Stereo => "stereo",
Self::TwoPointOne => "2.1",
Self::Quad => "quad",
Self::FivePointOneBack => "5.1(back)",
Self::FivePointOneSide => "5.1(side)",
Self::SevenPointOne => "7.1",
}
}
}
#[derive(Clone, Debug)]
pub struct WaveFormatExtensible {
pub wfx: WaveFormatEx,
pub valid_bits_per_sample: u16,
pub channel_mask: u32,
pub subformat: Guid,
}
pub fn parse_waveformatextensible(buf: &[u8]) -> Result<WaveFormatExtensible> {
let mut wfx = parse_waveformatex(buf)?;
if wfx.format_tag != WAVE_FORMAT_EXTENSIBLE {
return Err(Error::invalid(format!(
"AVI: WAVEFORMATEXTENSIBLE requires wFormatTag = 0x{:04X}, got 0x{:04X}",
WAVE_FORMAT_EXTENSIBLE, wfx.format_tag
)));
}
if wfx.extradata.len() < 22 {
return Err(Error::invalid(format!(
"AVI: WAVEFORMATEXTENSIBLE requires >= 22 bytes of cbSize extension, got {}",
wfx.extradata.len()
)));
}
let valid_bits_per_sample = u16::from_le_bytes([wfx.extradata[0], wfx.extradata[1]]);
let channel_mask = u32::from_le_bytes([
wfx.extradata[2],
wfx.extradata[3],
wfx.extradata[4],
wfx.extradata[5],
]);
let subformat = Guid::from_bytes(&wfx.extradata[6..22]).expect("checked >= 22 bytes above");
wfx.extradata = wfx.extradata[22..].to_vec();
Ok(WaveFormatExtensible {
wfx,
valid_bits_per_sample,
channel_mask,
subformat,
})
}
#[allow(clippy::too_many_arguments)]
pub fn write_waveformatextensible(
channels: u16,
samples_per_sec: u32,
avg_bytes_per_sec: u32,
block_align: u16,
container_bits: u16,
valid_bps: u16,
channel_mask: u32,
subformat: &Guid,
) -> Vec<u8> {
let mut extension = Vec::with_capacity(22);
extension.extend_from_slice(&valid_bps.to_le_bytes());
extension.extend_from_slice(&channel_mask.to_le_bytes());
extension.extend_from_slice(&subformat.0);
write_waveformatex(
WAVE_FORMAT_EXTENSIBLE,
channels,
samples_per_sec,
avg_bytes_per_sec,
block_align,
container_bits,
&extension,
)
}
pub fn subformat_codec_hint(subformat: &Guid, bits_per_sample: u16) -> Option<&'static str> {
if *subformat == KSDATAFORMAT_SUBTYPE_PCM {
return Some(match bits_per_sample {
8 => "pcm_u8",
24 => "pcm_s24le",
32 => "pcm_s32le",
_ => "pcm_s16le",
});
}
if *subformat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT {
return Some(match bits_per_sample {
64 => "pcm_f64le",
_ => "pcm_f32le",
});
}
if *subformat == KSDATAFORMAT_SUBTYPE_ALAW {
return Some("pcm_alaw");
}
if *subformat == KSDATAFORMAT_SUBTYPE_MULAW {
return Some("pcm_mulaw");
}
if *subformat == KSDATAFORMAT_SUBTYPE_ADPCM {
return Some("adpcm_ms");
}
if *subformat == KSDATAFORMAT_SUBTYPE_MPEG {
return Some("mp2");
}
if *subformat == KSDATAFORMAT_SUBTYPE_DRM {
return Some("drm");
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bmih_roundtrip() {
let bytes = write_bitmap_info_header(320, 240, *b"MJPG", 24, &[0xAA, 0xBB]);
assert_eq!(bytes.len(), 42);
let h = parse_bitmap_info_header(&bytes).unwrap();
assert_eq!(h.width, 320);
assert_eq!(h.height, 240);
assert_eq!(&h.compression, b"MJPG");
assert_eq!(h.bit_count, 24);
assert_eq!(h.extradata, vec![0xAA, 0xBB]);
assert!(!h.top_down);
}
#[test]
fn bmih_roundtrip_top_down() {
let bytes = write_bitmap_info_header_oriented(320, 240, [0, 0, 0, 0], 24, &[], true);
let h = parse_bitmap_info_header(&bytes).unwrap();
assert_eq!(h.height, 240, "abs(biHeight) reported as positive pixels");
assert!(h.top_down, "negative biHeight ⇒ top_down flag set");
let on_wire = i32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]);
assert_eq!(on_wire, -240);
}
#[test]
fn bitfields_masks_rgb565() {
let mut ext = Vec::new();
ext.extend_from_slice(&0x0000_F800u32.to_le_bytes()); ext.extend_from_slice(&0x0000_07E0u32.to_le_bytes()); ext.extend_from_slice(&0x0000_001Fu32.to_le_bytes()); let masks = parse_bitfields_masks(&ext).unwrap();
assert_eq!(masks, (0xF800, 0x07E0, 0x001F));
}
#[test]
fn bitfields_masks_too_short_returns_none() {
assert!(parse_bitfields_masks(&[1u8, 2, 3]).is_none());
assert!(parse_bitfields_masks(&[]).is_none());
}
#[test]
fn wfx_roundtrip() {
let bytes = write_waveformatex(1, 2, 48_000, 192_000, 4, 16, &[]);
assert_eq!(bytes.len(), 18);
let h = parse_waveformatex(&bytes).unwrap();
assert_eq!(h.format_tag, 1);
assert_eq!(h.channels, 2);
assert_eq!(h.samples_per_sec, 48_000);
assert_eq!(h.block_align, 4);
assert_eq!(h.bits_per_sample, 16);
}
#[test]
fn guid_display_canonical_form() {
let s = KSDATAFORMAT_SUBTYPE_PCM.display();
assert_eq!(s, "00000001-0000-0010-8000-00aa00389b71");
}
#[test]
fn guid_ksdataformat_base_pattern_and_tag_recovery() {
for (guid, expected_tag) in [
(KSDATAFORMAT_SUBTYPE_PCM, 0x0001u16),
(KSDATAFORMAT_SUBTYPE_IEEE_FLOAT, 0x0003),
(KSDATAFORMAT_SUBTYPE_DRM, 0x0009),
(KSDATAFORMAT_SUBTYPE_ALAW, 0x0006),
(KSDATAFORMAT_SUBTYPE_MULAW, 0x0007),
(KSDATAFORMAT_SUBTYPE_ADPCM, 0x0002),
(KSDATAFORMAT_SUBTYPE_MPEG, 0x0050),
] {
assert!(
guid.is_ksdataformat_base(),
"{} not KSDATAFORMAT base",
guid.display()
);
assert_eq!(guid.ksdataformat_tag(), Some(expected_tag));
}
let foreign = Guid::from_components(0xDEAD_BEEF, 0xCAFE, 0xBABE, [1, 2, 3, 4, 5, 6, 7, 8]);
assert!(!foreign.is_ksdataformat_base());
assert_eq!(foreign.ksdataformat_tag(), None);
}
#[test]
fn wfex_roundtrip_pcm_24in32() {
let channels = 6u16;
let sample_rate = 48_000u32;
let block_align = channels * 4; let avg_bps = sample_rate * block_align as u32;
let channel_mask: u32 = 0x0000_003F;
let bytes = write_waveformatextensible(
channels,
sample_rate,
avg_bps,
block_align,
32, 24, channel_mask,
&KSDATAFORMAT_SUBTYPE_PCM,
);
assert_eq!(bytes.len(), 40, "WAVEFORMATEXTENSIBLE is 18 + 2 + 22 bytes");
assert_eq!(u16::from_le_bytes([bytes[16], bytes[17]]), 22);
let parsed = parse_waveformatextensible(&bytes).unwrap();
assert_eq!(parsed.wfx.format_tag, WAVE_FORMAT_EXTENSIBLE);
assert_eq!(parsed.wfx.channels, channels);
assert_eq!(parsed.wfx.samples_per_sec, sample_rate);
assert_eq!(parsed.wfx.avg_bytes_per_sec, avg_bps);
assert_eq!(parsed.wfx.block_align, block_align);
assert_eq!(parsed.wfx.bits_per_sample, 32);
assert_eq!(parsed.valid_bits_per_sample, 24);
assert_eq!(parsed.channel_mask, channel_mask);
assert_eq!(parsed.subformat, KSDATAFORMAT_SUBTYPE_PCM);
assert!(parsed.wfx.extradata.is_empty());
}
#[test]
fn wfex_rejects_non_extensible_format_tag() {
let bytes = write_waveformatex(0x0001, 2, 48_000, 192_000, 4, 16, &[0u8; 22]);
assert!(parse_waveformatextensible(&bytes).is_err());
}
#[test]
fn wfex_rejects_short_extension() {
let bytes = write_waveformatex(
WAVE_FORMAT_EXTENSIBLE,
2,
48_000,
192_000,
4,
16,
&[0u8; 10],
);
assert!(parse_waveformatextensible(&bytes).is_err());
}
#[test]
fn subformat_codec_hint_pcm_depth_aware() {
assert_eq!(
subformat_codec_hint(&KSDATAFORMAT_SUBTYPE_PCM, 8),
Some("pcm_u8")
);
assert_eq!(
subformat_codec_hint(&KSDATAFORMAT_SUBTYPE_PCM, 16),
Some("pcm_s16le")
);
assert_eq!(
subformat_codec_hint(&KSDATAFORMAT_SUBTYPE_PCM, 24),
Some("pcm_s24le")
);
assert_eq!(
subformat_codec_hint(&KSDATAFORMAT_SUBTYPE_PCM, 32),
Some("pcm_s32le")
);
assert_eq!(
subformat_codec_hint(&KSDATAFORMAT_SUBTYPE_IEEE_FLOAT, 32),
Some("pcm_f32le")
);
assert_eq!(
subformat_codec_hint(&KSDATAFORMAT_SUBTYPE_IEEE_FLOAT, 64),
Some("pcm_f64le")
);
assert_eq!(
subformat_codec_hint(&KSDATAFORMAT_SUBTYPE_ALAW, 8),
Some("pcm_alaw")
);
assert_eq!(
subformat_codec_hint(&KSDATAFORMAT_SUBTYPE_MULAW, 8),
Some("pcm_mulaw")
);
assert_eq!(
subformat_codec_hint(&KSDATAFORMAT_SUBTYPE_ADPCM, 4),
Some("adpcm_ms")
);
assert_eq!(
subformat_codec_hint(&KSDATAFORMAT_SUBTYPE_MPEG, 0),
Some("mp2")
);
let foreign = Guid::from_components(0xDEAD, 0xBEEF, 0xCAFE, [0; 8]);
assert!(subformat_codec_hint(&foreign, 16).is_none());
}
#[test]
fn wfx_short_legacy() {
let mut bytes = Vec::new();
bytes.extend_from_slice(&0x0001u16.to_le_bytes());
bytes.extend_from_slice(&2u16.to_le_bytes());
bytes.extend_from_slice(&8_000u32.to_le_bytes());
bytes.extend_from_slice(&16_000u32.to_le_bytes());
bytes.extend_from_slice(&2u16.to_le_bytes());
let h = parse_waveformatex(&bytes).unwrap();
assert_eq!(h.bits_per_sample, 0);
assert!(h.extradata.is_empty());
}
#[test]
fn channel_mask_empty_and_layout_for_unrecognised() {
let m = ChannelMask::from_raw(0);
assert!(m.is_empty());
assert_eq!(m.len(), 0);
assert_eq!(m.iter_speakers().count(), 0);
assert!(m.layout().is_none());
assert_eq!(m.reserved_bits(), 0);
}
#[test]
fn channel_mask_iter_order_matches_docs_table() {
let m = ChannelMask::from_raw(0x0000_003F);
let got: Vec<Speaker> = m.iter_speakers().collect();
assert_eq!(
got,
vec![
Speaker::FrontLeft,
Speaker::FrontRight,
Speaker::FrontCenter,
Speaker::LowFrequency,
Speaker::BackLeft,
Speaker::BackRight,
],
"iter_speakers must follow lowest-set-bit-first PCM channel order"
);
assert_eq!(m.len(), 6);
assert_eq!(m.layout(), Some(ChannelLayout::FivePointOneBack));
}
#[test]
fn channel_mask_named_layouts_round_trip() {
for layout in [
ChannelLayout::Mono,
ChannelLayout::Stereo,
ChannelLayout::TwoPointOne,
ChannelLayout::Quad,
ChannelLayout::FivePointOneBack,
ChannelLayout::FivePointOneSide,
ChannelLayout::SevenPointOne,
] {
let mask = layout.mask();
let recovered = ChannelLayout::from_mask(mask);
assert_eq!(
recovered,
Some(layout),
"layout {:?} (mask 0x{:08X}) must round-trip",
layout,
mask
);
}
}
#[test]
fn channel_mask_layout_specific_values_match_docs_table() {
assert_eq!(ChannelLayout::Mono.mask(), 0x0000_0004);
assert_eq!(ChannelLayout::Stereo.mask(), 0x0000_0003);
assert_eq!(ChannelLayout::Quad.mask(), 0x0000_0033);
assert_eq!(ChannelLayout::FivePointOneBack.mask(), 0x0000_003F);
assert_eq!(ChannelLayout::FivePointOneSide.mask(), 0x0000_060F);
assert_eq!(ChannelLayout::SevenPointOne.mask(), 0x0000_063F);
}
#[test]
fn channel_mask_reserved_bits_isolated_and_ignored_for_layout() {
let stereo_plus_reserved = ChannelLayout::Stereo.mask() | 0x0040_0000;
let m = ChannelMask::from_raw(stereo_plus_reserved);
assert_eq!(m.layout(), Some(ChannelLayout::Stereo));
assert_eq!(m.reserved_bits(), 0x0040_0000);
let got: Vec<Speaker> = m.iter_speakers().collect();
assert_eq!(got, vec![Speaker::FrontLeft, Speaker::FrontRight]);
assert_eq!(m.len(), 2, "len counts only documented bits");
}
#[test]
fn channel_mask_speaker_all_surfaces_separately() {
let m = ChannelMask::from_raw(SPEAKER_ALL);
let got: Vec<Speaker> = m.iter_speakers().collect();
assert_eq!(got, vec![Speaker::SpeakerAll]);
assert_eq!(m.layout(), None);
assert_eq!(m.len(), 1);
assert_eq!(m.reserved_bits(), 0);
}
#[test]
fn speaker_abbrev_and_mask_bit_table_complete() {
let all = [
Speaker::FrontLeft,
Speaker::FrontRight,
Speaker::FrontCenter,
Speaker::LowFrequency,
Speaker::BackLeft,
Speaker::BackRight,
Speaker::FrontLeftOfCenter,
Speaker::FrontRightOfCenter,
Speaker::BackCenter,
Speaker::SideLeft,
Speaker::SideRight,
Speaker::TopCenter,
Speaker::TopFrontLeft,
Speaker::TopFrontCenter,
Speaker::TopFrontRight,
Speaker::TopBackLeft,
Speaker::TopBackCenter,
Speaker::TopBackRight,
Speaker::SpeakerAll,
];
let mut seen = 0u32;
for sp in all {
let bit = sp.mask_bit();
assert!(bit != 0, "{:?} mask bit must be non-zero", sp);
assert_eq!(
seen & bit,
0,
"{:?} bit 0x{:08X} overlaps a previous Speaker",
sp,
bit
);
seen |= bit;
assert!(!sp.abbrev().is_empty(), "{:?} abbrev must be non-empty", sp);
}
}
}