use oxideav_core::{
CodecId, CodecParameters, CodecResolver, Error, MediaType, Packet, Result, SampleFormat,
StreamInfo, TimeBase,
};
use oxideav_core::{ContainerRegistry, Demuxer, Muxer, ReadSeek, WriteSeek};
use std::io::{Read, Seek, SeekFrom, Write};
pub fn register(reg: &mut ContainerRegistry) {
reg.register_demuxer("wav", open_demuxer);
reg.register_muxer("wav", open_muxer);
reg.register_extension("wav", "wav");
reg.register_extension("wave", "wav");
reg.register_probe("wav", probe);
}
fn probe(p: &oxideav_core::ProbeData) -> u8 {
if p.buf.len() < 12 {
return 0;
}
let magic = &p.buf[0..4];
let is_known_form = magic == b"RIFF" || magic == b"RF64" || magic == b"BW64";
if is_known_form && &p.buf[8..12] == b"WAVE" {
100
} else {
0
}
}
const SIZE64_SENTINEL: u32 = 0xFFFF_FFFF;
#[derive(Default)]
struct Ds64 {
#[allow(dead_code)]
riff_size: u64,
data_size: u64,
sample_count: u64,
table: Vec<([u8; 4], u64)>,
}
fn parse_ds64_chunk(buf: &[u8], out: &mut Vec<(String, String)>) -> Result<Ds64> {
if buf.len() < 28 {
return Err(Error::invalid("RF64 ds64 chunk shorter than 28 bytes"));
}
let riff_size = u64::from_le_bytes([
buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7],
]);
let data_size = u64::from_le_bytes([
buf[8], buf[9], buf[10], buf[11], buf[12], buf[13], buf[14], buf[15],
]);
let sample_count = u64::from_le_bytes([
buf[16], buf[17], buf[18], buf[19], buf[20], buf[21], buf[22], buf[23],
]);
let table_len = u32::from_le_bytes([buf[24], buf[25], buf[26], buf[27]]) as usize;
out.push(("wav:rf64.riff_size".to_string(), riff_size.to_string()));
out.push(("wav:rf64.data_size".to_string(), data_size.to_string()));
out.push((
"wav:rf64.sample_count".to_string(),
sample_count.to_string(),
));
out.push(("wav:rf64.table.count".to_string(), table_len.to_string()));
const REC_LEN: usize = 12;
let mut table = Vec::with_capacity(table_len);
let table_bytes_available = buf.len().saturating_sub(28);
let table_recs_available = table_bytes_available / REC_LEN;
let n = table_len.min(table_recs_available);
for i in 0..n {
let off = 28 + i * REC_LEN;
let id: [u8; 4] = [buf[off], buf[off + 1], buf[off + 2], buf[off + 3]];
let size = u64::from_le_bytes([
buf[off + 4],
buf[off + 5],
buf[off + 6],
buf[off + 7],
buf[off + 8],
buf[off + 9],
buf[off + 10],
buf[off + 11],
]);
let id_str = if id.iter().all(|&b| (0x20..=0x7E).contains(&b)) {
String::from_utf8_lossy(&id).to_string()
} else {
format!("0x{:02X}{:02X}{:02X}{:02X}", id[0], id[1], id[2], id[3])
};
out.push((format!("wav:rf64.table.{i}.id"), id_str.clone()));
out.push((format!("wav:rf64.table.{i}.size"), size.to_string()));
table.push((id, size));
}
out.push(("wav:rf64.body_len".to_string(), buf.len().to_string()));
Ok(Ds64 {
riff_size,
data_size,
sample_count,
table,
})
}
fn resolve_chunk_size(id: &[u8; 4], on_wire: u32, ds64: Option<&Ds64>) -> Option<u64> {
if on_wire != SIZE64_SENTINEL {
return Some(on_wire as u64);
}
let ds64 = ds64?;
if id == b"data" {
return Some(ds64.data_size);
}
ds64.table
.iter()
.find(|(tid, _)| tid == id)
.map(|(_, sz)| *sz)
}
pub const WAVE_FORMAT_PCM: u16 = 0x0001;
pub const WAVE_FORMAT_IEEE_FLOAT: u16 = 0x0003;
pub const WAVE_FORMAT_ALAW: u16 = 0x0006;
pub const WAVE_FORMAT_MULAW: u16 = 0x0007;
pub const WAVE_FORMAT_EXTENSIBLE: u16 = 0xFFFE;
const FMT_PCM: u16 = WAVE_FORMAT_PCM;
const FMT_IEEE_FLOAT: u16 = WAVE_FORMAT_IEEE_FLOAT;
const FMT_ALAW: u16 = WAVE_FORMAT_ALAW;
const FMT_MULAW: u16 = WAVE_FORMAT_MULAW;
const FMT_EXTENSIBLE: u16 = WAVE_FORMAT_EXTENSIBLE;
const GUID_PCM: [u8; 16] = [
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71,
];
const GUID_IEEE_FLOAT: [u8; 16] = [
0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71,
];
const GUID_ALAW: [u8; 16] = [
0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71,
];
const GUID_MULAW: [u8; 16] = [
0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71,
];
const GUID_WAVEFORMATEX_TAIL: [u8; 14] = [
0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71,
];
fn waveformatex_tag(g: &[u8; 16]) -> Option<u16> {
if g[2..16] == GUID_WAVEFORMATEX_TAIL {
Some(u16::from_le_bytes([g[0], g[1]]))
} else {
None
}
}
fn fmt_guid(g: &[u8; 16]) -> String {
format!(
"{:08X}-{:04X}-{:04X}-{:02X}{:02X}-{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}",
u32::from_le_bytes([g[0], g[1], g[2], g[3]]),
u16::from_le_bytes([g[4], g[5]]),
u16::from_le_bytes([g[6], g[7]]),
g[8],
g[9],
g[10],
g[11],
g[12],
g[13],
g[14],
g[15],
)
}
const SPEAKER_FLAGS: [(u32, &str); 18] = [
(0x1, "FRONT_LEFT"),
(0x2, "FRONT_RIGHT"),
(0x4, "FRONT_CENTER"),
(0x8, "LOW_FREQUENCY"),
(0x10, "BACK_LEFT"),
(0x20, "BACK_RIGHT"),
(0x40, "FRONT_LEFT_OF_CENTER"),
(0x80, "FRONT_RIGHT_OF_CENTER"),
(0x100, "BACK_CENTER"),
(0x200, "SIDE_LEFT"),
(0x400, "SIDE_RIGHT"),
(0x800, "TOP_CENTER"),
(0x1000, "TOP_FRONT_LEFT"),
(0x2000, "TOP_FRONT_CENTER"),
(0x4000, "TOP_FRONT_RIGHT"),
(0x8000, "TOP_BACK_LEFT"),
(0x10000, "TOP_BACK_CENTER"),
(0x20000, "TOP_BACK_RIGHT"),
];
fn channel_mask_layout(mask: u32) -> Option<String> {
if mask == 0 {
return None;
}
let mut parts: Vec<String> = Vec::new();
let mut known: u32 = 0;
for (bit, name) in SPEAKER_FLAGS {
if mask & bit != 0 {
parts.push(name.to_string());
known |= bit;
}
}
let unknown = mask & !known;
if unknown != 0 {
parts.push(format!("UNKNOWN(0x{unknown:X})"));
}
Some(parts.join("+"))
}
fn open_demuxer(input: Box<dyn ReadSeek>, _codecs: &dyn CodecResolver) -> Result<Box<dyn Demuxer>> {
Ok(Box::new(open_wav_demuxer(input)?))
}
pub fn open_wav_demuxer(mut input: Box<dyn ReadSeek>) -> Result<WavDemuxer> {
let mut hdr = [0u8; 12];
input.read_exact(&mut hdr)?;
let magic: [u8; 4] = [hdr[0], hdr[1], hdr[2], hdr[3]];
let is_rf64 = &magic == b"RF64";
let is_bw64 = &magic == b"BW64";
let is_riff = &magic == b"RIFF";
if !(is_riff || is_rf64 || is_bw64) || &hdr[8..12] != b"WAVE" {
return Err(Error::invalid("not a RIFF/RF64/BW64 WAVE file"));
}
let mut fmt: Option<WaveFmt> = None;
let mut metadata: Vec<(String, String)> = Vec::new();
let mut fact_sample_count: Option<u64> = None;
let mut acid: Option<AcidChunk> = None;
let mut ds64: Option<Ds64> = None;
if is_rf64 || is_bw64 {
let form = if is_rf64 { "RF64" } else { "BW64" };
metadata.push(("wav:rf64.magic".to_string(), form.to_string()));
let on_wire_riff = u32::from_le_bytes([hdr[4], hdr[5], hdr[6], hdr[7]]);
if on_wire_riff != SIZE64_SENTINEL {
metadata.push(("wav:rf64.riff_size32".to_string(), on_wire_riff.to_string()));
}
}
let mut data_offset: Option<u64> = None;
let mut data_size: Option<u64> = None;
loop {
let mut chdr = [0u8; 8];
match input.read_exact(&mut chdr) {
Ok(()) => {}
Err(ref e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
Err(e) => return Err(e.into()),
}
let id_arr: [u8; 4] = [chdr[0], chdr[1], chdr[2], chdr[3]];
let id = &chdr[0..4];
let on_wire_size = u32::from_le_bytes([chdr[4], chdr[5], chdr[6], chdr[7]]);
let size = match resolve_chunk_size(&id_arr, on_wire_size, ds64.as_ref()) {
Some(s) => s,
None => {
return Err(Error::invalid(format!(
"RF64 chunk {:?} carries 0xFFFFFFFF sentinel but no ds64 override",
String::from_utf8_lossy(id)
)));
}
};
match id {
b"ds64" => {
let mut buf = vec![0u8; size as usize];
input.read_exact(&mut buf)?;
let parsed = parse_ds64_chunk(&buf, &mut metadata)?;
ds64 = Some(parsed);
if size % 2 == 1 {
input.seek(SeekFrom::Current(1))?;
}
}
b"fmt " => {
let mut buf = vec![0u8; size as usize];
input.read_exact(&mut buf)?;
fmt = Some(parse_fmt(&buf)?);
if size % 2 == 1 {
input.seek(SeekFrom::Current(1))?;
}
}
b"fact" => {
let mut buf = vec![0u8; size as usize];
input.read_exact(&mut buf)?;
let legacy = parse_fact_chunk(&buf, &mut metadata);
fact_sample_count = match legacy {
Some(v) if v == SIZE64_SENTINEL => ds64.as_ref().map(|d| d.sample_count),
Some(v) => Some(v as u64),
None => None,
};
if size % 2 == 1 {
input.seek(SeekFrom::Current(1))?;
}
}
b"LIST" => {
let list_start = input.stream_position()?;
let mut buf = vec![0u8; size as usize];
input.read_exact(&mut buf)?;
if buf.len() >= 4 && &buf[0..4] == b"wavl" {
if let Some((off, sz)) = parse_wavl_list(&buf, list_start, &mut metadata) {
if data_offset.is_none() {
data_offset = Some(off);
data_size = Some(sz);
}
}
} else {
parse_list_chunk(&buf, &mut metadata);
}
if size % 2 == 1 {
input.seek(SeekFrom::Current(1))?;
}
}
b"bext" => {
let mut buf = vec![0u8; size as usize];
input.read_exact(&mut buf)?;
parse_bext_chunk(&buf, &mut metadata);
if size % 2 == 1 {
input.seek(SeekFrom::Current(1))?;
}
}
b"cue " => {
let mut buf = vec![0u8; size as usize];
input.read_exact(&mut buf)?;
parse_cue_chunk(&buf, &mut metadata);
if size % 2 == 1 {
input.seek(SeekFrom::Current(1))?;
}
}
b"plst" => {
let mut buf = vec![0u8; size as usize];
input.read_exact(&mut buf)?;
parse_plst_chunk(&buf, &mut metadata);
if size % 2 == 1 {
input.seek(SeekFrom::Current(1))?;
}
}
b"smpl" => {
let mut buf = vec![0u8; size as usize];
input.read_exact(&mut buf)?;
parse_smpl_chunk(&buf, &mut metadata);
if size % 2 == 1 {
input.seek(SeekFrom::Current(1))?;
}
}
b"inst" => {
let mut buf = vec![0u8; size as usize];
input.read_exact(&mut buf)?;
parse_inst_chunk(&buf, &mut metadata);
if size % 2 == 1 {
input.seek(SeekFrom::Current(1))?;
}
}
b"acid" => {
let mut buf = vec![0u8; size as usize];
input.read_exact(&mut buf)?;
acid = parse_acid_chunk(&buf, &mut metadata);
if size % 2 == 1 {
input.seek(SeekFrom::Current(1))?;
}
}
b"iXML" => {
let mut buf = vec![0u8; size as usize];
input.read_exact(&mut buf)?;
parse_ixml_chunk(&buf, &mut metadata);
if size % 2 == 1 {
input.seek(SeekFrom::Current(1))?;
}
}
b"axml" => {
let mut buf = vec![0u8; size as usize];
input.read_exact(&mut buf)?;
parse_axml_chunk(&buf, &mut metadata);
if size % 2 == 1 {
input.seek(SeekFrom::Current(1))?;
}
}
b"_PMX" => {
let mut buf = vec![0u8; size as usize];
input.read_exact(&mut buf)?;
parse_pmx_chunk(&buf, &mut metadata);
if size % 2 == 1 {
input.seek(SeekFrom::Current(1))?;
}
}
b"CSET" => {
let mut buf = vec![0u8; size as usize];
input.read_exact(&mut buf)?;
parse_cset_chunk(&buf, &mut metadata);
if size % 2 == 1 {
input.seek(SeekFrom::Current(1))?;
}
}
b"JUNK" => {
input.seek(SeekFrom::Current(size as i64))?;
surface_junk_metadata(&mut metadata, size);
if size % 2 == 1 {
input.seek(SeekFrom::Current(1))?;
}
}
b"slnt" => {
let mut buf = vec![0u8; size as usize];
input.read_exact(&mut buf)?;
surface_slnt_metadata(&mut metadata, &buf);
if size % 2 == 1 {
input.seek(SeekFrom::Current(1))?;
}
}
b"data" => {
data_offset = Some(input.stream_position()?);
data_size = Some(size);
break;
}
_ => {
let pad = size + (size % 2);
input.seek(SeekFrom::Current(pad as i64))?;
}
}
}
let fmt = fmt.ok_or_else(|| Error::invalid("WAV missing fmt chunk"))?;
let data_offset =
data_offset.ok_or_else(|| Error::invalid("WAV missing data / wavl waveform"))?;
let data_size = data_size.unwrap_or(0);
let codec_id = resolve_codec(&fmt)?;
let sample_fmt = decoded_sample_format(&codec_id);
let time_base = TimeBase::new(1, fmt.sample_rate as i64);
let block_align = fmt.block_align.max(1) as u64;
let block_samples = data_size / block_align;
let total_samples = if let Some(fc) = fact_sample_count {
if fc != block_samples {
metadata.push((
"wav:fact.mismatch".to_string(),
format!("block_samples={block_samples} fact_samples={fc}"),
));
}
fc
} else {
block_samples
};
let duration_micros: i64 = if fmt.sample_rate > 0 {
(total_samples as i128 * 1_000_000 / fmt.sample_rate as i128) as i64
} else {
0
};
let mut params = CodecParameters::audio(codec_id);
params.tag = Some(oxideav_core::CodecTag::wave_format(fmt.format_tag));
params.channels = Some(fmt.channels);
params.sample_rate = Some(fmt.sample_rate);
params.sample_format = sample_fmt;
params.bit_rate = Some(8 * block_align * (fmt.sample_rate as u64));
if fmt.format_tag == FMT_EXTENSIBLE {
if let Some(valid) = fmt.valid_bits_per_sample {
metadata.push((
"wav:fmt.valid_bits_per_sample".to_string(),
valid.to_string(),
));
}
if let Some(mask) = fmt.channel_mask {
metadata.push(("wav:fmt.channel_mask".to_string(), format!("0x{mask:08X}")));
if let Some(layout) = channel_mask_layout(mask) {
metadata.push(("wav:fmt.channel_layout".to_string(), layout));
}
}
if let Some(sub) = &fmt.subformat {
metadata.push(("wav:fmt.subformat".to_string(), fmt_guid(sub)));
if let Some(tag) = waveformatex_tag(sub) {
metadata.push(("wav:fmt.subformat_tag".to_string(), format!("0x{tag:04X}")));
}
}
}
let stream = StreamInfo {
index: 0,
time_base,
duration: Some(total_samples as i64),
start_time: Some(0),
params,
};
Ok(WavDemuxer {
input,
streams: vec![stream],
data_offset,
data_end: data_offset + data_size,
cursor: data_offset,
block_align,
chunk_frames: 1024,
samples_emitted: 0,
metadata,
duration_micros,
format_tag: fmt.format_tag,
valid_bits_per_sample: fmt.valid_bits_per_sample,
channel_mask: fmt.channel_mask,
subformat: fmt.subformat,
acid,
})
}
fn parse_list_chunk(buf: &[u8], out: &mut Vec<(String, String)>) {
if buf.len() < 4 {
return;
}
match &buf[0..4] {
b"INFO" => parse_info_list(&buf[4..], out),
b"adtl" => parse_adtl_list(&buf[4..], out),
_ => {}
}
}
fn parse_wavl_list(
buf: &[u8],
list_start: u64,
out: &mut Vec<(String, String)>,
) -> Option<(u64, u64)> {
let mut i = 4usize;
let mut first_data: Option<(u64, u64)> = None;
let mut segment_count: u64 = 0;
let mut data_count: u64 = 0;
let mut data_bytes: u64 = 0;
while i + 8 <= buf.len() {
let id: [u8; 4] = [buf[i], buf[i + 1], buf[i + 2], buf[i + 3]];
let size = u32::from_le_bytes([buf[i + 4], buf[i + 5], buf[i + 6], buf[i + 7]]) as usize;
let body = i + 8;
if body + size > buf.len() {
break;
}
let idx = segment_count;
match &id {
b"data" => {
out.push((format!("wav:wavl.{idx}.kind"), "data".to_string()));
out.push((format!("wav:wavl.{idx}.length"), size.to_string()));
data_count += 1;
data_bytes = data_bytes.saturating_add(size as u64);
if first_data.is_none() {
first_data = Some((list_start + body as u64, size as u64));
}
}
b"slnt" => {
out.push((format!("wav:wavl.{idx}.kind"), "slnt".to_string()));
out.push((format!("wav:wavl.{idx}.length"), size.to_string()));
surface_slnt_metadata(out, &buf[body..body + size]);
}
_ => {
out.push((
format!("wav:wavl.{idx}.kind"),
String::from_utf8_lossy(&id).trim().to_string(),
));
out.push((format!("wav:wavl.{idx}.length"), size.to_string()));
}
}
segment_count += 1;
i = body + size + (size & 1);
}
out.push((
"wav:wavl.segment_count".to_string(),
segment_count.to_string(),
));
out.push(("wav:wavl.data_count".to_string(), data_count.to_string()));
out.push(("wav:wavl.data_bytes".to_string(), data_bytes.to_string()));
first_data
}
fn parse_info_list(buf: &[u8], out: &mut Vec<(String, String)>) {
let mut i = 0usize;
while i + 8 <= buf.len() {
let id: [u8; 4] = [buf[i], buf[i + 1], buf[i + 2], buf[i + 3]];
let size = u32::from_le_bytes([buf[i + 4], buf[i + 5], buf[i + 6], buf[i + 7]]) as usize;
i += 8;
if i + size > buf.len() {
break;
}
let raw = &buf[i..i + size];
let end = raw.iter().position(|&b| b == 0).unwrap_or(raw.len());
let value = String::from_utf8_lossy(&raw[..end]).trim().to_string();
let key = info_id_to_key(&id);
if !value.is_empty() {
if let Some(k) = key {
out.push((k.to_string(), value));
}
}
i += size;
if size % 2 == 1 {
i += 1;
}
}
}
fn parse_adtl_list(buf: &[u8], out: &mut Vec<(String, String)>) {
let mut i = 0usize;
while i + 8 <= buf.len() {
let id: [u8; 4] = [buf[i], buf[i + 1], buf[i + 2], buf[i + 3]];
let size = u32::from_le_bytes([buf[i + 4], buf[i + 5], buf[i + 6], buf[i + 7]]) as usize;
i += 8;
if i + size > buf.len() {
break;
}
let body = &buf[i..i + size];
match &id {
b"labl" | b"note" if body.len() >= 4 => {
let dw_name = u32::from_le_bytes([body[0], body[1], body[2], body[3]]);
let raw = &body[4..];
let end = raw.iter().position(|&b| b == 0).unwrap_or(raw.len());
let text = String::from_utf8_lossy(&raw[..end]).trim().to_string();
if !text.is_empty() {
let sub = if &id == b"labl" { "labl" } else { "note" };
out.push((format!("wav:adtl.{sub}.{dw_name}"), text));
}
}
b"ltxt" if body.len() >= 20 => {
let dw_name = u32::from_le_bytes([body[0], body[1], body[2], body[3]]);
let dw_length = u32::from_le_bytes([body[4], body[5], body[6], body[7]]);
let purpose = &body[8..12];
out.push((
format!("wav:adtl.ltxt.{dw_name}.length"),
dw_length.to_string(),
));
let purpose_str = if purpose.iter().all(|&b| (0x20..=0x7E).contains(&b)) {
String::from_utf8_lossy(purpose).to_string()
} else {
format!(
"0x{:02X}{:02X}{:02X}{:02X}",
purpose[0], purpose[1], purpose[2], purpose[3]
)
};
out.push((format!("wav:adtl.ltxt.{dw_name}.purpose"), purpose_str));
let country = u16::from_le_bytes([body[12], body[13]]);
let language = u16::from_le_bytes([body[14], body[15]]);
let dialect = u16::from_le_bytes([body[16], body[17]]);
let code_page = u16::from_le_bytes([body[18], body[19]]);
out.push((
format!("wav:adtl.ltxt.{dw_name}.country"),
country.to_string(),
));
if let Some(name) = cset_country_name(country) {
out.push((
format!("wav:adtl.ltxt.{dw_name}.country_name"),
name.to_string(),
));
}
out.push((
format!("wav:adtl.ltxt.{dw_name}.language"),
language.to_string(),
));
out.push((
format!("wav:adtl.ltxt.{dw_name}.dialect"),
dialect.to_string(),
));
if let Some(name) = cset_language_name(language, dialect) {
out.push((
format!("wav:adtl.ltxt.{dw_name}.language_name"),
name.to_string(),
));
}
out.push((
format!("wav:adtl.ltxt.{dw_name}.code_page"),
code_page.to_string(),
));
let raw = &body[20..];
let end = raw.iter().position(|&b| b == 0).unwrap_or(raw.len());
let text = String::from_utf8_lossy(&raw[..end]).trim().to_string();
if !text.is_empty() {
out.push((format!("wav:adtl.ltxt.{dw_name}.text"), text));
}
}
b"file" if body.len() >= 8 => {
let dw_name = u32::from_le_bytes([body[0], body[1], body[2], body[3]]);
let med_type = &body[4..8];
let med_type_str = if med_type == [0, 0, 0, 0] {
"0".to_string()
} else if med_type.iter().all(|&b| (0x20..=0x7E).contains(&b)) {
String::from_utf8_lossy(med_type).to_string()
} else {
format!(
"0x{:02X}{:02X}{:02X}{:02X}",
med_type[0], med_type[1], med_type[2], med_type[3]
)
};
out.push((format!("wav:adtl.file.{dw_name}.med_type"), med_type_str));
out.push((
format!("wav:adtl.file.{dw_name}.body_len"),
(body.len() - 8).to_string(),
));
}
_ => {}
}
i += size;
if size % 2 == 1 {
i += 1;
}
}
}
fn parse_cue_chunk(buf: &[u8], out: &mut Vec<(String, String)>) {
if buf.len() < 4 {
return;
}
let count_claimed = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
let body = &buf[4..];
const REC_LEN: usize = 24;
let count_actual = (body.len() / REC_LEN) as u32;
let count = count_claimed.min(count_actual);
out.push(("wav:cue.count".to_string(), count.to_string()));
for i in 0..count as usize {
let off = i * REC_LEN;
let dw_name = u32::from_le_bytes([body[off], body[off + 1], body[off + 2], body[off + 3]]);
let dw_position =
u32::from_le_bytes([body[off + 4], body[off + 5], body[off + 6], body[off + 7]]);
let fcc_chunk = &body[off + 8..off + 12];
let dw_chunk_start = u32::from_le_bytes([
body[off + 12],
body[off + 13],
body[off + 14],
body[off + 15],
]);
let dw_block_start = u32::from_le_bytes([
body[off + 16],
body[off + 17],
body[off + 18],
body[off + 19],
]);
let dw_sample_offset = u32::from_le_bytes([
body[off + 20],
body[off + 21],
body[off + 22],
body[off + 23],
]);
let fcc_str = if fcc_chunk.iter().all(|&b| (0x20..=0x7E).contains(&b)) {
String::from_utf8_lossy(fcc_chunk).to_string()
} else {
format!(
"0x{:02X}{:02X}{:02X}{:02X}",
fcc_chunk[0], fcc_chunk[1], fcc_chunk[2], fcc_chunk[3]
)
};
out.push((
format!("wav:cue.{dw_name}.position"),
dw_position.to_string(),
));
out.push((format!("wav:cue.{dw_name}.fcc_chunk"), fcc_str));
out.push((
format!("wav:cue.{dw_name}.chunk_start"),
dw_chunk_start.to_string(),
));
out.push((
format!("wav:cue.{dw_name}.block_start"),
dw_block_start.to_string(),
));
out.push((
format!("wav:cue.{dw_name}.sample_offset"),
dw_sample_offset.to_string(),
));
}
}
fn parse_plst_chunk(buf: &[u8], out: &mut Vec<(String, String)>) {
if buf.len() < 4 {
return;
}
let count_claimed = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
let body = &buf[4..];
const REC_LEN: usize = 12;
let count_actual = (body.len() / REC_LEN) as u32;
let count = count_claimed.min(count_actual);
out.push(("wav:plst.count".to_string(), count.to_string()));
for i in 0..count as usize {
let off = i * REC_LEN;
let dw_name = u32::from_le_bytes([body[off], body[off + 1], body[off + 2], body[off + 3]]);
let dw_length =
u32::from_le_bytes([body[off + 4], body[off + 5], body[off + 6], body[off + 7]]);
let dw_loops =
u32::from_le_bytes([body[off + 8], body[off + 9], body[off + 10], body[off + 11]]);
out.push((format!("wav:plst.{i}.cue_id"), dw_name.to_string()));
out.push((format!("wav:plst.{i}.length"), dw_length.to_string()));
out.push((format!("wav:plst.{i}.loops"), dw_loops.to_string()));
}
}
fn parse_smpl_chunk(buf: &[u8], out: &mut Vec<(String, String)>) {
const FIXED_LEN: usize = 36;
const LOOP_LEN: usize = 24;
if buf.len() < FIXED_LEN {
return;
}
let r = |off: usize| -> u32 {
u32::from_le_bytes([buf[off], buf[off + 1], buf[off + 2], buf[off + 3]])
};
let manufacturer = r(0);
let product = r(4);
let sample_period = r(8);
let midi_unity_note = r(12);
let midi_pitch_fraction = r(16);
let smpte_format = r(20);
let smpte_offset = r(24);
let num_loops_claimed = r(28);
let sampler_data_len = r(32);
out.push((
"wav:smpl.manufacturer".to_string(),
manufacturer.to_string(),
));
out.push(("wav:smpl.product".to_string(), product.to_string()));
out.push((
"wav:smpl.sample_period".to_string(),
sample_period.to_string(),
));
out.push((
"wav:smpl.midi_unity_note".to_string(),
midi_unity_note.to_string(),
));
out.push((
"wav:smpl.midi_pitch_fraction".to_string(),
midi_pitch_fraction.to_string(),
));
out.push((
"wav:smpl.smpte_format".to_string(),
smpte_format.to_string(),
));
let smpte_hh = (smpte_offset >> 24) & 0xFF;
let smpte_mm = (smpte_offset >> 16) & 0xFF;
let smpte_ss = (smpte_offset >> 8) & 0xFF;
let smpte_ff = smpte_offset & 0xFF;
out.push((
"wav:smpl.smpte_offset".to_string(),
format!("{smpte_hh:02}:{smpte_mm:02}:{smpte_ss:02}:{smpte_ff:02}"),
));
out.push((
"wav:smpl.sampler_data_len".to_string(),
sampler_data_len.to_string(),
));
let body = &buf[FIXED_LEN..];
let num_loops_fits = (body.len() / LOOP_LEN) as u32;
let num_loops = num_loops_claimed.min(num_loops_fits);
out.push((
"wav:smpl.num_sample_loops".to_string(),
num_loops.to_string(),
));
for i in 0..num_loops as usize {
let off = i * LOOP_LEN;
let loop_field = |word: usize| -> u32 {
let p = off + word * 4;
u32::from_le_bytes([body[p], body[p + 1], body[p + 2], body[p + 3]])
};
let cue_point_id = loop_field(0);
let loop_type = loop_field(1);
let start = loop_field(2);
let end = loop_field(3);
let fraction = loop_field(4);
let play_count = loop_field(5);
out.push((
format!("wav:smpl.loop.{i}.cue_point_id"),
cue_point_id.to_string(),
));
out.push((format!("wav:smpl.loop.{i}.type"), loop_type.to_string()));
out.push((format!("wav:smpl.loop.{i}.start"), start.to_string()));
out.push((format!("wav:smpl.loop.{i}.end"), end.to_string()));
out.push((format!("wav:smpl.loop.{i}.fraction"), fraction.to_string()));
out.push((
format!("wav:smpl.loop.{i}.play_count"),
play_count.to_string(),
));
}
}
fn parse_inst_chunk(buf: &[u8], out: &mut Vec<(String, String)>) {
const FIXED_LEN: usize = 7;
if buf.len() < FIXED_LEN {
return;
}
let unshifted_note = buf[0];
let fine_tune = buf[1] as i8;
let gain = buf[2] as i8;
let low_note = buf[3];
let high_note = buf[4];
let low_velocity = buf[5];
let high_velocity = buf[6];
out.push((
"wav:inst.unshifted_note".to_string(),
unshifted_note.to_string(),
));
out.push(("wav:inst.fine_tune".to_string(), fine_tune.to_string()));
out.push(("wav:inst.gain".to_string(), gain.to_string()));
out.push(("wav:inst.low_note".to_string(), low_note.to_string()));
out.push(("wav:inst.high_note".to_string(), high_note.to_string()));
out.push((
"wav:inst.low_velocity".to_string(),
low_velocity.to_string(),
));
out.push((
"wav:inst.high_velocity".to_string(),
high_velocity.to_string(),
));
}
pub const ACID_FLAG_ONE_SHOT: u32 = 1 << 0;
pub const ACID_FLAG_ROOT_NOTE_SET: u32 = 1 << 1;
pub const ACID_FLAG_STRETCH: u32 = 1 << 2;
pub const ACID_FLAG_DISK_BASED: u32 = 1 << 3;
pub const ACID_FLAG_HIGH_OCTAVE: u32 = 1 << 4;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AcidChunk {
pub flags: u32,
pub root_note: u16,
pub reserved: [u8; 6],
pub num_beats: u32,
pub meter: u32,
pub tempo: f32,
}
impl AcidChunk {
pub const BODY_LEN: usize = 24;
pub fn one_shot(&self) -> bool {
self.flags & ACID_FLAG_ONE_SHOT != 0
}
pub fn root_note_set(&self) -> bool {
self.flags & ACID_FLAG_ROOT_NOTE_SET != 0
}
pub fn stretch(&self) -> bool {
self.flags & ACID_FLAG_STRETCH != 0
}
pub fn disk_based(&self) -> bool {
self.flags & ACID_FLAG_DISK_BASED != 0
}
pub fn high_octave(&self) -> bool {
self.flags & ACID_FLAG_HIGH_OCTAVE != 0
}
pub fn root_note_name(&self) -> Option<&'static str> {
const NAMES: [&str; 24] = [
"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B", "High C", "High C#",
"High D", "High D#", "High E", "High F", "High F#", "High G", "High G#", "High A",
"High A#", "High B",
];
NAMES.get(self.root_note.wrapping_sub(48) as usize).copied()
}
pub fn parse(buf: &[u8]) -> Option<AcidChunk> {
if buf.len() < Self::BODY_LEN {
return None;
}
let r32 =
|o: usize| -> u32 { u32::from_le_bytes([buf[o], buf[o + 1], buf[o + 2], buf[o + 3]]) };
let mut reserved = [0u8; 6];
reserved.copy_from_slice(&buf[6..12]);
Some(AcidChunk {
flags: r32(0),
root_note: u16::from_le_bytes([buf[4], buf[5]]),
reserved,
num_beats: r32(12),
meter: r32(16),
tempo: f32::from_le_bytes([buf[20], buf[21], buf[22], buf[23]]),
})
}
pub fn to_bytes(&self) -> [u8; 24] {
let mut out = [0u8; 24];
out[0..4].copy_from_slice(&self.flags.to_le_bytes());
out[4..6].copy_from_slice(&self.root_note.to_le_bytes());
out[6..12].copy_from_slice(&self.reserved);
out[12..16].copy_from_slice(&self.num_beats.to_le_bytes());
out[16..20].copy_from_slice(&self.meter.to_le_bytes());
out[20..24].copy_from_slice(&self.tempo.to_le_bytes());
out
}
}
fn parse_acid_chunk(buf: &[u8], out: &mut Vec<(String, String)>) -> Option<AcidChunk> {
let acid = AcidChunk::parse(buf)?;
out.push((
"wav:acid.flags".to_string(),
format!("0x{:08X}", acid.flags),
));
out.push((
"wav:acid.one_shot".to_string(),
(acid.one_shot() as u8).to_string(),
));
out.push((
"wav:acid.root_note_set".to_string(),
(acid.root_note_set() as u8).to_string(),
));
out.push((
"wav:acid.stretch".to_string(),
(acid.stretch() as u8).to_string(),
));
out.push((
"wav:acid.disk_based".to_string(),
(acid.disk_based() as u8).to_string(),
));
out.push((
"wav:acid.high_octave".to_string(),
(acid.high_octave() as u8).to_string(),
));
out.push(("wav:acid.root_note".to_string(), acid.root_note.to_string()));
if let Some(name) = acid.root_note_name() {
out.push(("wav:acid.root_note_name".to_string(), name.to_string()));
}
out.push(("wav:acid.num_beats".to_string(), acid.num_beats.to_string()));
out.push(("wav:acid.meter".to_string(), acid.meter.to_string()));
out.push(("wav:acid.tempo".to_string(), acid.tempo.to_string()));
if acid.reserved.iter().any(|&b| b != 0) {
let hex: String = acid.reserved.iter().map(|b| format!("{b:02X}")).collect();
out.push(("wav:acid.reserved".to_string(), hex));
}
if buf.len() > AcidChunk::BODY_LEN {
out.push(("wav:acid.body_len".to_string(), buf.len().to_string()));
}
Some(acid)
}
fn parse_fact_chunk(buf: &[u8], out: &mut Vec<(String, String)>) -> Option<u32> {
if buf.len() < 4 {
return None;
}
let sample_count = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
out.push((
"wav:fact.sample_count".to_string(),
sample_count.to_string(),
));
if buf.len() > 4 {
out.push(("wav:fact.body_len".to_string(), buf.len().to_string()));
}
Some(sample_count)
}
fn parse_ixml_chunk(buf: &[u8], out: &mut Vec<(String, String)>) {
out.push(("wav:ixml.body_len".to_string(), buf.len().to_string()));
let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
let text = String::from_utf8_lossy(&buf[..end]).trim().to_string();
if !text.is_empty() {
out.push(("wav:ixml".to_string(), text));
}
}
fn parse_axml_chunk(buf: &[u8], out: &mut Vec<(String, String)>) {
out.push(("wav:axml.body_len".to_string(), buf.len().to_string()));
let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
let text = String::from_utf8_lossy(&buf[..end]).trim().to_string();
if !text.is_empty() {
out.push(("wav:axml".to_string(), text));
}
}
fn parse_pmx_chunk(buf: &[u8], out: &mut Vec<(String, String)>) {
out.push(("wav:xmp.body_len".to_string(), buf.len().to_string()));
let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
let text = String::from_utf8_lossy(&buf[..end]).trim().to_string();
if !text.is_empty() {
out.push(("wav:xmp".to_string(), text));
}
}
fn surface_junk_metadata(out: &mut Vec<(String, String)>, size: u64) {
let mut count: u64 = 0;
let mut total: u64 = 0;
for (k, v) in out.iter() {
if k == "wav:junk.count" {
count = v.parse().unwrap_or(count);
} else if k == "wav:junk.total_bytes" {
total = v.parse().unwrap_or(total);
}
}
let idx = count;
count = count.saturating_add(1);
total = total.saturating_add(size);
out.push((format!("wav:junk.{idx}.body_len"), size.to_string()));
out.retain(|(k, _)| k != "wav:junk.count" && k != "wav:junk.total_bytes");
out.push(("wav:junk.count".to_string(), count.to_string()));
out.push(("wav:junk.total_bytes".to_string(), total.to_string()));
}
fn surface_slnt_metadata(out: &mut Vec<(String, String)>, buf: &[u8]) {
let mut count: u64 = 0;
let mut total: u64 = 0;
for (k, v) in out.iter() {
if k == "wav:slnt.count" {
count = v.parse().unwrap_or(count);
} else if k == "wav:slnt.total_samples" {
total = v.parse().unwrap_or(total);
}
}
let idx = count;
count = count.saturating_add(1);
if buf.len() >= 4 {
let samples = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
total = total.saturating_add(samples as u64);
out.push((format!("wav:slnt.{idx}.samples"), samples.to_string()));
}
out.retain(|(k, _)| k != "wav:slnt.count" && k != "wav:slnt.total_samples");
out.push(("wav:slnt.count".to_string(), count.to_string()));
out.push(("wav:slnt.total_samples".to_string(), total.to_string()));
}
fn cset_country_name(code: u16) -> Option<&'static str> {
match code {
0 => Some("None"),
1 => Some("USA"),
2 => Some("Canada"),
3 => Some("Latin America"),
30 => Some("Greece"),
31 => Some("Netherlands"),
32 => Some("Belgium"),
33 => Some("France"),
34 => Some("Spain"),
39 => Some("Italy"),
41 => Some("Switzerland"),
43 => Some("Austria"),
44 => Some("United Kingdom"),
45 => Some("Denmark"),
46 => Some("Sweden"),
47 => Some("Norway"),
49 => Some("West Germany"),
52 => Some("Mexico"),
55 => Some("Brazil"),
61 => Some("Australia"),
64 => Some("New Zealand"),
81 => Some("Japan"),
82 => Some("Korea"),
86 => Some("People's Republic of China"),
88 => Some("Taiwan"),
90 => Some("Turkey"),
351 => Some("Portugal"),
352 => Some("Luxembourg"),
354 => Some("Iceland"),
358 => Some("Finland"),
_ => None,
}
}
fn cset_language_name(language: u16, dialect: u16) -> Option<&'static str> {
match (language, dialect) {
(0, _) => Some("None"),
(1, 1) => Some("Arabic"),
(2, 1) => Some("Bulgarian"),
(3, 1) => Some("Catalan"),
(4, 1) => Some("Traditional Chinese"),
(4, 2) => Some("Simplified Chinese"),
(5, 1) => Some("Czech"),
(6, 1) => Some("Danish"),
(7, 1) => Some("German"),
(7, 2) => Some("Swiss German"),
(8, 1) => Some("Greek"),
(9, 1) => Some("US English"),
(9, 2) => Some("UK English"),
(10, 1) => Some("Spanish"),
(10, 2) => Some("Spanish Mexican"),
(11, 1) => Some("Finnish"),
(12, 1) => Some("French"),
(12, 2) => Some("Belgian French"),
(12, 3) => Some("Canadian French"),
(12, 4) => Some("Swiss French"),
(13, 1) => Some("Hebrew"),
(14, 1) => Some("Hungarian"),
(15, 1) => Some("Icelandic"),
(16, 1) => Some("Italian"),
(16, 2) => Some("Swiss Italian"),
(17, 1) => Some("Japanese"),
(18, 1) => Some("Korean"),
(19, 1) => Some("Dutch"),
(19, 2) => Some("Belgian Dutch"),
(20, 1) => Some("Norwegian - Bokmal"),
(20, 2) => Some("Norwegian - Nynorsk"),
(21, 1) => Some("Polish"),
(22, 1) => Some("Brazilian Portuguese"),
(22, 2) => Some("Portuguese"),
(23, 1) => Some("Rhaeto-Romanic"),
(24, 1) => Some("Romanian"),
(25, 1) => Some("Russian"),
(26, 1) => Some("Serbo-Croatian (Latin)"),
(26, 2) => Some("Serbo-Croatian (Cyrillic)"),
(27, 1) => Some("Slovak"),
(28, 1) => Some("Albanian"),
(29, 1) => Some("Swedish"),
(30, 1) => Some("Thai"),
(31, 1) => Some("Turkish"),
(32, 1) => Some("Urdu"),
(33, 1) => Some("Bahasa"),
_ => None,
}
}
fn parse_cset_chunk(buf: &[u8], out: &mut Vec<(String, String)>) {
out.push(("wav:cset.body_len".to_string(), buf.len().to_string()));
if buf.len() < 8 {
return;
}
let code_page = u16::from_le_bytes([buf[0], buf[1]]);
let country = u16::from_le_bytes([buf[2], buf[3]]);
let language = u16::from_le_bytes([buf[4], buf[5]]);
let dialect = u16::from_le_bytes([buf[6], buf[7]]);
out.push(("wav:cset.code_page".to_string(), code_page.to_string()));
out.push(("wav:cset.country".to_string(), country.to_string()));
if let Some(name) = cset_country_name(country) {
out.push(("wav:cset.country_name".to_string(), name.to_string()));
}
out.push(("wav:cset.language".to_string(), language.to_string()));
out.push(("wav:cset.dialect".to_string(), dialect.to_string()));
if let Some(name) = cset_language_name(language, dialect) {
out.push(("wav:cset.language_name".to_string(), name.to_string()));
}
}
fn info_id_to_key(id: &[u8; 4]) -> Option<&'static str> {
match id {
b"IARL" => Some("archival_location"),
b"IART" => Some("artist"),
b"ICMS" => Some("commissioned"),
b"ICMT" => Some("comment"),
b"ICOP" => Some("copyright"),
b"ICRD" => Some("date"),
b"ICRP" => Some("cropped"),
b"IDIM" => Some("dimensions"),
b"IDPI" => Some("dpi"),
b"IENG" => Some("engineer"),
b"IGNR" => Some("genre"),
b"IKEY" => Some("keywords"),
b"ILGT" => Some("lightness"),
b"IMED" => Some("medium"),
b"INAM" => Some("title"),
b"IPLT" => Some("palette_setting"),
b"IPRD" => Some("album"),
b"ISBJ" => Some("subject"),
b"ISFT" => Some("encoder"),
b"ISHP" => Some("sharpness"),
b"ISRC" => Some("source"),
b"ISRF" => Some("source_form"),
b"ITCH" => Some("technician"),
b"ITRK" => Some("track"),
_ => None,
}
}
const BEXT_FIXED_LEN: usize = 602;
fn bext_field(raw: &[u8]) -> String {
let end = raw.iter().position(|&b| b == 0).unwrap_or(raw.len());
String::from_utf8_lossy(&raw[..end]).trim().to_string()
}
fn parse_bext_chunk(buf: &[u8], out: &mut Vec<(String, String)>) {
if buf.len() < BEXT_FIXED_LEN {
return;
}
let description = bext_field(&buf[0..256]);
let originator = bext_field(&buf[256..288]);
let originator_ref = bext_field(&buf[288..320]);
let origination_date = bext_field(&buf[320..330]);
let origination_time = bext_field(&buf[330..338]);
let time_ref_low = u32::from_le_bytes([buf[338], buf[339], buf[340], buf[341]]);
let time_ref_high = u32::from_le_bytes([buf[342], buf[343], buf[344], buf[345]]);
let time_reference = ((time_ref_high as u64) << 32) | (time_ref_low as u64);
let version = u16::from_le_bytes([buf[346], buf[347]]);
let umid = &buf[348..412];
let loudness_value = i16::from_le_bytes([buf[412], buf[413]]);
let loudness_range = i16::from_le_bytes([buf[414], buf[415]]);
let max_true_peak = i16::from_le_bytes([buf[416], buf[417]]);
let max_momentary = i16::from_le_bytes([buf[418], buf[419]]);
let max_short_term = i16::from_le_bytes([buf[420], buf[421]]);
let coding_history = if buf.len() > BEXT_FIXED_LEN {
bext_field(&buf[BEXT_FIXED_LEN..])
} else {
String::new()
};
let push = |out: &mut Vec<(String, String)>, key: &str, value: String| {
if !value.is_empty() {
out.push((key.to_string(), value));
}
};
push(out, "wav:bext.description", description);
push(out, "wav:bext.originator", originator);
push(out, "wav:bext.originator_reference", originator_ref);
push(out, "wav:bext.origination_date", origination_date);
push(out, "wav:bext.origination_time", origination_time);
out.push((
"wav:bext.time_reference".to_string(),
time_reference.to_string(),
));
out.push(("wav:bext.version".to_string(), version.to_string()));
if umid.iter().any(|&b| b != 0) {
let mut hex = String::with_capacity(umid.len() * 2);
for b in umid {
hex.push_str(&format!("{b:02x}"));
}
out.push(("wav:bext.umid".to_string(), hex));
}
if version >= 2 {
out.push((
"wav:bext.loudness_value".to_string(),
fmt_loudness(loudness_value),
));
out.push((
"wav:bext.loudness_range".to_string(),
fmt_loudness(loudness_range),
));
out.push((
"wav:bext.max_true_peak_level".to_string(),
fmt_loudness(max_true_peak),
));
out.push((
"wav:bext.max_momentary_loudness".to_string(),
fmt_loudness(max_momentary),
));
out.push((
"wav:bext.max_short_term_loudness".to_string(),
fmt_loudness(max_short_term),
));
}
push(out, "wav:bext.coding_history", coding_history);
}
fn fmt_loudness(v: i16) -> String {
let neg = v < 0;
let abs = (v as i32).unsigned_abs();
let whole = abs / 100;
let frac = abs % 100;
if neg {
format!("-{whole}.{frac:02}")
} else {
format!("{whole}.{frac:02}")
}
}
#[derive(Clone, Debug)]
struct WaveFmt {
format_tag: u16,
channels: u16,
sample_rate: u32,
#[allow(dead_code)]
byte_rate: u32,
block_align: u16,
bits_per_sample: u16,
valid_bits_per_sample: Option<u16>,
channel_mask: Option<u32>,
subformat: Option<[u8; 16]>,
}
fn parse_fmt(buf: &[u8]) -> Result<WaveFmt> {
if buf.len() < 16 {
return Err(Error::invalid("fmt chunk too small"));
}
let format_tag = u16::from_le_bytes([buf[0], buf[1]]);
let channels = u16::from_le_bytes([buf[2], buf[3]]);
let sample_rate = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]);
let byte_rate = 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 = u16::from_le_bytes([buf[14], buf[15]]);
let mut valid_bits_per_sample = None;
let mut channel_mask = None;
let mut subformat = None;
if format_tag == FMT_EXTENSIBLE {
if buf.len() < 40 {
return Err(Error::invalid(
"WAVE_FORMAT_EXTENSIBLE fmt chunk shorter than the 40 bytes \
mandated by mmreg.h",
));
}
let cb_size = u16::from_le_bytes([buf[16], buf[17]]);
if (cb_size as usize) < 22 {
return Err(Error::invalid(format!(
"WAVE_FORMAT_EXTENSIBLE cbSize must be >= 22, got {cb_size}"
)));
}
valid_bits_per_sample = Some(u16::from_le_bytes([buf[18], buf[19]]));
channel_mask = Some(u32::from_le_bytes([buf[20], buf[21], buf[22], buf[23]]));
let mut g = [0u8; 16];
g.copy_from_slice(&buf[24..40]);
subformat = Some(g);
}
Ok(WaveFmt {
format_tag,
channels,
sample_rate,
byte_rate,
block_align,
bits_per_sample,
valid_bits_per_sample,
channel_mask,
subformat,
})
}
fn codec_for_tag(tag: u16, bits: u16) -> Result<Option<CodecId>> {
Ok(match tag {
FMT_PCM => Some(CodecId::new(pcm_int_codec(bits)?)),
FMT_IEEE_FLOAT => Some(CodecId::new(pcm_float_codec(bits)?)),
FMT_ALAW => Some(CodecId::new("pcm_alaw")),
FMT_MULAW => Some(CodecId::new("pcm_mulaw")),
_ => None,
})
}
fn resolve_codec(fmt: &WaveFmt) -> Result<CodecId> {
if fmt.format_tag != FMT_EXTENSIBLE {
return match codec_for_tag(fmt.format_tag, fmt.bits_per_sample)? {
Some(id) => Ok(id),
None => Err(Error::unsupported(format!(
"unsupported WAV format tag 0x{:04x}",
fmt.format_tag
))),
};
}
let sub = fmt
.subformat
.ok_or_else(|| Error::invalid("extensible WAV missing subformat"))?;
let depth = fmt
.valid_bits_per_sample
.filter(|&v| v > 0)
.unwrap_or(fmt.bits_per_sample);
if let Some(tag) = waveformatex_tag(&sub) {
if tag != FMT_EXTENSIBLE {
if let Some(id) = codec_for_tag(tag, depth)? {
return Ok(id);
}
}
}
Ok(CodecId::new(format!("wav:guid_{}", fmt_guid(&sub))))
}
fn pcm_int_codec(bits: u16) -> Result<&'static str> {
Ok(match bits {
8 => "pcm_u8",
16 => "pcm_s16le",
24 => "pcm_s24le",
32 => "pcm_s32le",
_ => {
return Err(Error::unsupported(format!(
"unsupported WAV integer-PCM bit depth: {bits}"
)));
}
})
}
fn pcm_float_codec(bits: u16) -> Result<&'static str> {
Ok(match bits {
32 => "pcm_f32le",
64 => "pcm_f64le",
_ => {
return Err(Error::unsupported(format!(
"unsupported WAV IEEE-float bit depth: {bits}"
)));
}
})
}
fn decoded_sample_format(id: &CodecId) -> Option<SampleFormat> {
if let Some(fmt) = super::pcm::sample_format_for(id) {
return Some(fmt);
}
match id.as_str() {
"pcm_alaw" | "pcm_mulaw" => Some(SampleFormat::S16),
_ => None,
}
}
pub struct WavDemuxer {
input: Box<dyn ReadSeek>,
streams: Vec<StreamInfo>,
data_offset: u64,
data_end: u64,
cursor: u64,
block_align: u64,
chunk_frames: u64,
samples_emitted: i64,
metadata: Vec<(String, String)>,
duration_micros: i64,
format_tag: u16,
valid_bits_per_sample: Option<u16>,
channel_mask: Option<u32>,
subformat: Option<[u8; 16]>,
acid: Option<AcidChunk>,
}
impl WavDemuxer {
pub fn format_tag(&self) -> u16 {
self.format_tag
}
pub fn valid_bits_per_sample(&self) -> Option<u16> {
self.valid_bits_per_sample
}
pub fn channel_mask(&self) -> Option<u32> {
self.channel_mask
}
pub fn channel_layout(&self) -> Option<String> {
self.channel_mask.and_then(channel_mask_layout)
}
pub fn subformat(&self) -> Option<&[u8; 16]> {
self.subformat.as_ref()
}
pub fn subformat_text(&self) -> Option<String> {
self.subformat.as_ref().map(fmt_guid)
}
pub fn acid(&self) -> Option<&AcidChunk> {
self.acid.as_ref()
}
}
impl Demuxer for WavDemuxer {
fn format_name(&self) -> &str {
"wav"
}
fn streams(&self) -> &[StreamInfo] {
&self.streams
}
fn next_packet(&mut self) -> Result<Packet> {
if self.cursor >= self.data_end {
return Err(Error::Eof);
}
let remaining = self.data_end - self.cursor;
let want_bytes = (self.chunk_frames * self.block_align).min(remaining);
let want_bytes = (want_bytes / self.block_align) * self.block_align;
if want_bytes == 0 {
return Err(Error::Eof);
}
self.input.seek(SeekFrom::Start(self.cursor))?;
let mut buf = vec![0u8; want_bytes as usize];
self.input.read_exact(&mut buf)?;
self.cursor += want_bytes;
let stream = &self.streams[0];
let frames = want_bytes / self.block_align;
let pts = self.samples_emitted;
self.samples_emitted += frames as i64;
let mut pkt = Packet::new(0, stream.time_base, buf);
pkt.pts = Some(pts);
pkt.dts = Some(pts);
pkt.duration = Some(frames as i64);
pkt.flags.keyframe = true;
Ok(pkt)
}
fn seek_to(&mut self, stream_index: u32, pts: i64) -> Result<i64> {
if stream_index != 0 {
return Err(Error::invalid(format!(
"WAV: stream index {stream_index} out of range"
)));
}
let total_samples = (self.data_end - self.data_offset) / self.block_align;
let target = (pts.max(0) as u64).min(total_samples);
let new_cursor = self.data_offset + target * self.block_align;
self.input.seek(SeekFrom::Start(new_cursor))?;
self.cursor = new_cursor;
self.samples_emitted = target as i64;
Ok(target as i64)
}
fn metadata(&self) -> &[(String, String)] {
&self.metadata
}
fn duration_micros(&self) -> Option<i64> {
if self.duration_micros > 0 {
Some(self.duration_micros)
} else {
None
}
}
}
fn open_muxer(output: Box<dyn WriteSeek>, streams: &[StreamInfo]) -> Result<Box<dyn Muxer>> {
if streams.len() != 1 {
return Err(Error::unsupported("WAV supports exactly one audio stream"));
}
let s = &streams[0];
if s.params.media_type != MediaType::Audio {
return Err(Error::invalid("WAV stream must be audio"));
}
let channels = s
.params
.channels
.ok_or_else(|| Error::invalid("WAV muxer: missing channels"))?;
let sample_rate = s
.params
.sample_rate
.ok_or_else(|| Error::invalid("WAV muxer: missing sample rate"))?;
let shape = wire_shape_for_params(&s.params)?;
Ok(Box::new(WavMuxer {
output,
channels,
sample_rate,
shape,
extensible: None,
acid: None,
riff_size_offset: 0,
data_size_offset: 0,
fact_size_offset: None,
data_bytes: 0,
header_written: false,
trailer_written: false,
}))
}
#[derive(Clone, Debug, Default)]
pub struct WavMuxOptions {
extensible: Option<ExtensibleOpts>,
acid: Option<AcidChunk>,
}
#[derive(Clone, Debug)]
struct ExtensibleOpts {
channel_mask: u32,
valid_bits_per_sample: Option<u16>,
subformat: Option<[u8; 16]>,
}
impl WavMuxOptions {
pub fn with_extensible(mut self, channel_mask: u32) -> Self {
self.extensible = Some(ExtensibleOpts {
channel_mask,
valid_bits_per_sample: None,
subformat: None,
});
self
}
pub fn with_valid_bits_per_sample(mut self, valid_bps: u16) -> Self {
if let Some(opts) = self.extensible.as_mut() {
opts.valid_bits_per_sample = Some(valid_bps);
}
self
}
pub fn with_subformat(mut self, guid: [u8; 16]) -> Self {
if let Some(opts) = self.extensible.as_mut() {
opts.subformat = Some(guid);
}
self
}
pub fn with_acid(mut self, acid: AcidChunk) -> Self {
self.acid = Some(acid);
self
}
}
pub fn open_muxer_with(
output: Box<dyn WriteSeek>,
streams: &[StreamInfo],
opts: WavMuxOptions,
) -> Result<Box<dyn Muxer>> {
if streams.len() != 1 {
return Err(Error::unsupported("WAV supports exactly one audio stream"));
}
let s = &streams[0];
if s.params.media_type != MediaType::Audio {
return Err(Error::invalid("WAV stream must be audio"));
}
let channels = s
.params
.channels
.ok_or_else(|| Error::invalid("WAV muxer: missing channels"))?;
let sample_rate = s
.params
.sample_rate
.ok_or_else(|| Error::invalid("WAV muxer: missing sample rate"))?;
let shape = wire_shape_for_params(&s.params)?;
Ok(Box::new(WavMuxer {
output,
channels,
sample_rate,
shape,
extensible: opts.extensible,
acid: opts.acid,
riff_size_offset: 0,
data_size_offset: 0,
fact_size_offset: None,
data_bytes: 0,
header_written: false,
trailer_written: false,
}))
}
#[derive(Clone, Copy, Debug)]
enum WireShape {
IntPcm { bits: u16 },
FloatPcm { bits: u16 },
Alaw,
Mulaw,
}
impl WireShape {
fn bits_per_sample(self) -> u16 {
match self {
WireShape::IntPcm { bits } | WireShape::FloatPcm { bits } => bits,
WireShape::Alaw | WireShape::Mulaw => 8,
}
}
fn format_tag(self) -> u16 {
match self {
WireShape::IntPcm { .. } => FMT_PCM,
WireShape::FloatPcm { .. } => FMT_IEEE_FLOAT,
WireShape::Alaw => FMT_ALAW,
WireShape::Mulaw => FMT_MULAW,
}
}
fn well_known_guid(self) -> [u8; 16] {
match self {
WireShape::IntPcm { .. } => GUID_PCM,
WireShape::FloatPcm { .. } => GUID_IEEE_FLOAT,
WireShape::Alaw => GUID_ALAW,
WireShape::Mulaw => GUID_MULAW,
}
}
}
fn wire_shape_for_params(p: &CodecParameters) -> Result<WireShape> {
match p.codec_id.as_str() {
"pcm_alaw" => return Ok(WireShape::Alaw),
"pcm_mulaw" => return Ok(WireShape::Mulaw),
_ => {}
}
let fmt = p
.sample_format
.or_else(|| super::pcm::sample_format_for(&p.codec_id))
.ok_or_else(|| Error::unsupported(format!("WAV: unknown PCM codec {}", p.codec_id)))?;
Ok(match fmt {
SampleFormat::U8 => WireShape::IntPcm { bits: 8 },
SampleFormat::S16 => WireShape::IntPcm { bits: 16 },
SampleFormat::S24 => WireShape::IntPcm { bits: 24 },
SampleFormat::S32 => WireShape::IntPcm { bits: 32 },
SampleFormat::F32 => WireShape::FloatPcm { bits: 32 },
SampleFormat::F64 => WireShape::FloatPcm { bits: 64 },
other => {
return Err(Error::unsupported(format!(
"WAV muxer cannot write sample format {:?}",
other
)));
}
})
}
struct WavMuxer {
output: Box<dyn WriteSeek>,
channels: u16,
sample_rate: u32,
shape: WireShape,
extensible: Option<ExtensibleOpts>,
acid: Option<AcidChunk>,
riff_size_offset: u64,
data_size_offset: u64,
fact_size_offset: Option<u64>,
data_bytes: u64,
header_written: bool,
trailer_written: bool,
}
impl Muxer for WavMuxer {
fn format_name(&self) -> &str {
"wav"
}
fn write_header(&mut self) -> Result<()> {
if self.header_written {
return Err(Error::other("WAV header already written"));
}
let bits_per_sample = self.shape.bits_per_sample();
let block_align = (bits_per_sample / 8) * self.channels;
let byte_rate = self.sample_rate * block_align as u32;
let format_tag = if self.extensible.is_some() {
FMT_EXTENSIBLE
} else {
self.shape.format_tag()
};
self.output.write_all(b"RIFF")?;
self.riff_size_offset = self.output.stream_position()?;
self.output.write_all(&0u32.to_le_bytes())?; self.output.write_all(b"WAVE")?;
let fmt_size: u32 = if self.extensible.is_some() { 40 } else { 16 };
self.output.write_all(b"fmt ")?;
self.output.write_all(&fmt_size.to_le_bytes())?;
self.output.write_all(&format_tag.to_le_bytes())?;
self.output.write_all(&self.channels.to_le_bytes())?;
self.output.write_all(&self.sample_rate.to_le_bytes())?;
self.output.write_all(&byte_rate.to_le_bytes())?;
self.output.write_all(&block_align.to_le_bytes())?;
self.output.write_all(&bits_per_sample.to_le_bytes())?;
if let Some(opts) = &self.extensible {
self.output.write_all(&22u16.to_le_bytes())?;
let valid = opts.valid_bits_per_sample.unwrap_or(bits_per_sample);
self.output.write_all(&valid.to_le_bytes())?;
self.output.write_all(&opts.channel_mask.to_le_bytes())?;
let guid = opts
.subformat
.unwrap_or_else(|| self.shape.well_known_guid());
self.output.write_all(&guid)?;
}
if format_tag != FMT_PCM {
self.output.write_all(b"fact")?;
self.output.write_all(&4u32.to_le_bytes())?;
self.fact_size_offset = Some(self.output.stream_position()?);
self.output.write_all(&0u32.to_le_bytes())?; }
if let Some(acid) = &self.acid {
self.output.write_all(b"acid")?;
self.output
.write_all(&(AcidChunk::BODY_LEN as u32).to_le_bytes())?;
self.output.write_all(&acid.to_bytes())?;
}
self.output.write_all(b"data")?;
self.data_size_offset = self.output.stream_position()?;
self.output.write_all(&0u32.to_le_bytes())?;
self.header_written = true;
Ok(())
}
fn write_packet(&mut self, packet: &Packet) -> Result<()> {
if !self.header_written {
return Err(Error::other("WAV muxer: write_header not called"));
}
self.output.write_all(&packet.data)?;
self.data_bytes += packet.data.len() as u64;
Ok(())
}
fn write_trailer(&mut self) -> Result<()> {
if self.trailer_written {
return Ok(());
}
if self.data_bytes % 2 == 1 {
self.output.write_all(&[0u8])?;
}
let end = self.output.stream_position()?;
let data_size_u32: u32 = self
.data_bytes
.try_into()
.map_err(|_| Error::other("WAV data chunk exceeds 4 GiB"))?;
self.output.seek(SeekFrom::Start(self.data_size_offset))?;
self.output.write_all(&data_size_u32.to_le_bytes())?;
if let Some(off) = self.fact_size_offset {
let bits_per_sample = self.shape.bits_per_sample() as u64;
let block_align = (bits_per_sample / 8) * self.channels as u64;
let sample_count = self.data_bytes.checked_div(block_align).unwrap_or(0);
let sample_count_u32: u32 = sample_count
.try_into()
.map_err(|_| Error::other("WAV fact sample count exceeds u32"))?;
self.output.seek(SeekFrom::Start(off))?;
self.output.write_all(&sample_count_u32.to_le_bytes())?;
}
let riff_size_u32: u32 = (end - 8)
.try_into()
.map_err(|_| Error::other("WAV RIFF size exceeds 4 GiB"))?;
self.output.seek(SeekFrom::Start(self.riff_size_offset))?;
self.output.write_all(&riff_size_u32.to_le_bytes())?;
self.output.seek(SeekFrom::Start(end))?;
self.output.flush()?;
self.trailer_written = true;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use oxideav_core::{CodecParameters, MediaType};
fn make_stream(fmt: SampleFormat, ch: u16, sr: u32) -> StreamInfo {
let mut params = CodecParameters::audio(super::super::pcm::codec_id_for(fmt).unwrap());
params.media_type = MediaType::Audio;
params.channels = Some(ch);
params.sample_rate = Some(sr);
params.sample_format = Some(fmt);
StreamInfo {
index: 0,
time_base: TimeBase::new(1, sr as i64),
duration: None,
start_time: Some(0),
params,
}
}
fn wave_format_tag(p: &CodecParameters) -> Option<u16> {
match p.tag.as_ref()? {
oxideav_core::CodecTag::WaveFormat(t) => Some(*t),
_ => None,
}
}
fn make_g711_stream(codec: &str, ch: u16, sr: u32) -> StreamInfo {
let mut params = CodecParameters::audio(CodecId::new(codec));
params.media_type = MediaType::Audio;
params.channels = Some(ch);
params.sample_rate = Some(sr);
params.sample_format = Some(SampleFormat::S16);
StreamInfo {
index: 0,
time_base: TimeBase::new(1, sr as i64),
duration: None,
start_time: Some(0),
params,
}
}
fn mux_to_bytes(
stream: &StreamInfo,
payload: &[u8],
opts: WavMuxOptions,
tag: &str,
) -> Vec<u8> {
let tmp = std::env::temp_dir().join(format!("oxideav-basic-wav-r77-{tag}.wav"));
let _ = std::fs::remove_file(&tmp);
{
let f = std::fs::File::create(&tmp).unwrap();
let ws: Box<dyn WriteSeek> = Box::new(f);
let mut mux = open_muxer_with(ws, std::slice::from_ref(stream), opts).unwrap();
mux.write_header().unwrap();
let pkt = Packet::new(0, stream.time_base, payload.to_vec());
mux.write_packet(&pkt).unwrap();
mux.write_trailer().unwrap();
}
let bytes = std::fs::read(&tmp).unwrap();
let _ = std::fs::remove_file(&tmp);
bytes
}
fn open_demux_from_bytes(bytes: Vec<u8>) -> Box<dyn Demuxer> {
use std::io::Cursor;
let rs: Box<dyn ReadSeek> = Box::new(Cursor::new(bytes));
open_demuxer(rs, &oxideav_core::NullCodecResolver).unwrap()
}
#[test]
fn round_trip_s16_mono() {
let samples: Vec<i16> = (0..1000).map(|i| ((i * 32) - 16000) as i16).collect();
let mut payload = Vec::with_capacity(samples.len() * 2);
for s in &samples {
payload.extend_from_slice(&s.to_le_bytes());
}
let stream = make_stream(SampleFormat::S16, 1, 48_000);
let tmp = std::env::temp_dir().join("oxideav-basic-wav-test.wav");
{
let f = std::fs::File::create(&tmp).unwrap();
let ws: Box<dyn WriteSeek> = Box::new(f);
let mut mux = open_muxer(ws, std::slice::from_ref(&stream)).unwrap();
mux.write_header().unwrap();
let pkt = Packet::new(0, stream.time_base, payload.clone());
mux.write_packet(&pkt).unwrap();
mux.write_trailer().unwrap();
}
let rs: Box<dyn ReadSeek> = Box::new(std::fs::File::open(&tmp).unwrap());
let mut dmx = open_demuxer(rs, &oxideav_core::NullCodecResolver).unwrap();
assert_eq!(dmx.format_name(), "wav");
assert_eq!(dmx.streams().len(), 1);
assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
let mut out_bytes = Vec::new();
loop {
match dmx.next_packet() {
Ok(p) => out_bytes.extend_from_slice(&p.data),
Err(Error::Eof) => break,
Err(e) => panic!("demux error: {e}"),
}
}
assert_eq!(out_bytes, payload);
}
#[test]
fn round_trip_alaw_mono() {
let payload: Vec<u8> = (0..=255u8).collect();
let stream = make_g711_stream("pcm_alaw", 1, 8_000);
let bytes = mux_to_bytes(&stream, &payload, WavMuxOptions::default(), "alaw-mono");
let mut dmx = open_demux_from_bytes(bytes);
assert_eq!(dmx.streams().len(), 1);
let s = &dmx.streams()[0];
assert_eq!(s.params.codec_id, CodecId::new("pcm_alaw"));
assert_eq!(wave_format_tag(&s.params), Some(FMT_ALAW));
assert_eq!(s.params.sample_format, Some(SampleFormat::S16));
assert_eq!(s.params.bit_rate, Some(8 * 8_000));
let mut out = Vec::new();
loop {
match dmx.next_packet() {
Ok(p) => out.extend_from_slice(&p.data),
Err(Error::Eof) => break,
Err(e) => panic!("demux error: {e}"),
}
}
assert_eq!(out, payload);
}
#[test]
fn round_trip_mulaw_stereo() {
let payload: Vec<u8> = (0..512u32).map(|i| (i & 0xFF) as u8).collect();
let stream = make_g711_stream("pcm_mulaw", 2, 8_000);
let bytes = mux_to_bytes(&stream, &payload, WavMuxOptions::default(), "mulaw-stereo");
let mut dmx = open_demux_from_bytes(bytes);
let s = &dmx.streams()[0];
assert_eq!(s.params.codec_id, CodecId::new("pcm_mulaw"));
assert_eq!(wave_format_tag(&s.params), Some(FMT_MULAW));
assert_eq!(s.params.bit_rate, Some(16 * 8_000));
let mut out = Vec::new();
loop {
match dmx.next_packet() {
Ok(p) => out.extend_from_slice(&p.data),
Err(Error::Eof) => break,
Err(e) => panic!("demux error: {e}"),
}
}
assert_eq!(out, payload);
}
#[test]
fn round_trip_extensible_5_1_pcm() {
const MASK_5_1: u32 = 0x0003F;
let frame: [i16; 6] = [-100, 200, -300, 400, -500, 600];
let mut payload = Vec::new();
for _ in 0..32 {
for s in &frame {
payload.extend_from_slice(&s.to_le_bytes());
}
}
let mut stream = make_stream(SampleFormat::S16, 6, 48_000);
stream.params.codec_id = CodecId::new("pcm_s16le");
let opts = WavMuxOptions::default().with_extensible(MASK_5_1);
let buf = mux_to_bytes(&stream, &payload, opts, "ext-5-1-pcm");
assert_eq!(&buf[12..16], b"fmt ");
let fmt_size = u32::from_le_bytes([buf[16], buf[17], buf[18], buf[19]]);
assert_eq!(fmt_size, 40, "EXTENSIBLE fmt chunk must be 40 bytes");
assert_eq!(u16::from_le_bytes([buf[20], buf[21]]), FMT_EXTENSIBLE);
assert_eq!(u16::from_le_bytes([buf[36], buf[37]]), 22);
let dmx = open_demux_from_bytes(buf);
let s = &dmx.streams()[0];
assert_eq!(s.params.codec_id, CodecId::new("pcm_s16le"));
assert_eq!(wave_format_tag(&s.params), Some(FMT_EXTENSIBLE));
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(
md.get("wav:fmt.channel_mask"),
Some(&format!("0x{MASK_5_1:08X}"))
);
assert_eq!(
md.get("wav:fmt.valid_bits_per_sample"),
Some(&"16".to_string())
);
assert_eq!(
md.get("wav:fmt.subformat"),
Some(&"00000001-0000-0010-8000-00AA00389B71".to_string())
);
assert_eq!(
md.get("wav:fmt.channel_layout"),
Some(
&"FRONT_LEFT+FRONT_RIGHT+FRONT_CENTER+LOW_FREQUENCY+BACK_LEFT+BACK_RIGHT"
.to_string()
)
);
}
#[test]
fn extensible_accessors_match_metadata() {
const MASK_STEREO: u32 = 0x00003;
let payload = vec![0u8; 4 * 100];
let mut stream = make_stream(SampleFormat::S16, 2, 44_100);
stream.params.codec_id = CodecId::new("pcm_s16le");
let opts = WavMuxOptions::default().with_extensible(MASK_STEREO);
let bytes = mux_to_bytes(&stream, &payload, opts, "ext-stereo-md");
let dmx = open_demux_from_bytes(bytes.clone());
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(
md.get("wav:fmt.channel_mask"),
Some(&format!("0x{MASK_STEREO:08X}"))
);
assert_eq!(
md.get("wav:fmt.valid_bits_per_sample"),
Some(&"16".to_string())
);
assert_eq!(
md.get("wav:fmt.channel_layout"),
Some(&"FRONT_LEFT+FRONT_RIGHT".to_string())
);
let typed = open_wav_demuxer(Box::new(std::io::Cursor::new(bytes))).unwrap();
assert_eq!(
typed.channel_layout(),
Some("FRONT_LEFT+FRONT_RIGHT".to_string())
);
assert_eq!(typed.channel_mask(), Some(MASK_STEREO));
}
#[test]
fn channel_mask_layout_decoding() {
assert_eq!(channel_mask_layout(0), None);
assert_eq!(channel_mask_layout(0x1), Some("FRONT_LEFT".to_string()));
assert_eq!(channel_mask_layout(0x8), Some("LOW_FREQUENCY".to_string()));
assert_eq!(
channel_mask_layout(0x20000),
Some("TOP_BACK_RIGHT".to_string())
);
assert_eq!(channel_mask_layout(0x4), Some("FRONT_CENTER".to_string()));
assert_eq!(
channel_mask_layout(0x33),
Some("FRONT_LEFT+FRONT_RIGHT+BACK_LEFT+BACK_RIGHT".to_string())
);
assert_eq!(
channel_mask_layout(0x63F),
Some(
"FRONT_LEFT+FRONT_RIGHT+FRONT_CENTER+LOW_FREQUENCY\
+BACK_LEFT+BACK_RIGHT+SIDE_LEFT+SIDE_RIGHT"
.to_string()
)
);
assert_eq!(
channel_mask_layout(0x40001),
Some("FRONT_LEFT+UNKNOWN(0x40000)".to_string())
);
assert_eq!(
channel_mask_layout(0x80000000),
Some("UNKNOWN(0x80000000)".to_string())
);
}
#[test]
fn extensible_alaw_through_guid() {
let payload: Vec<u8> = (0..=255u8).collect();
let stream = make_g711_stream("pcm_alaw", 1, 8_000);
let opts = WavMuxOptions::default().with_extensible(0x00004); let bytes = mux_to_bytes(&stream, &payload, opts, "ext-alaw");
let mut dmx = open_demux_from_bytes(bytes);
let s = &dmx.streams()[0];
assert_eq!(s.params.codec_id, CodecId::new("pcm_alaw"));
assert_eq!(wave_format_tag(&s.params), Some(FMT_EXTENSIBLE));
let mut out = Vec::new();
loop {
match dmx.next_packet() {
Ok(p) => out.extend_from_slice(&p.data),
Err(Error::Eof) => break,
Err(e) => panic!("demux error: {e}"),
}
}
assert_eq!(out, payload);
}
#[test]
fn extensible_unknown_guid_synthesised_id() {
let bogus_guid: [u8; 16] = [
0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC,
0xDE, 0xF0,
];
let mut buf = Vec::new();
buf.extend_from_slice(b"RIFF");
buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(b"WAVE");
buf.extend_from_slice(b"fmt ");
buf.extend_from_slice(&40u32.to_le_bytes()); buf.extend_from_slice(&FMT_EXTENSIBLE.to_le_bytes()); buf.extend_from_slice(&1u16.to_le_bytes()); buf.extend_from_slice(&44_100u32.to_le_bytes()); buf.extend_from_slice(&88_200u32.to_le_bytes()); buf.extend_from_slice(&2u16.to_le_bytes()); buf.extend_from_slice(&16u16.to_le_bytes()); buf.extend_from_slice(&22u16.to_le_bytes()); buf.extend_from_slice(&16u16.to_le_bytes()); buf.extend_from_slice(&0x00004u32.to_le_bytes()); buf.extend_from_slice(&bogus_guid); buf.extend_from_slice(b"data");
buf.extend_from_slice(&0u32.to_le_bytes());
use std::io::Cursor;
let rs: Box<dyn ReadSeek> = Box::new(Cursor::new(buf));
let dmx_res = open_demuxer(rs, &oxideav_core::NullCodecResolver);
let dmx = dmx_res.expect("unknown-GUID extensible stream still parses");
let id = dmx.streams()[0].params.codec_id.as_str().to_string();
assert!(
id.starts_with("wav:guid_"),
"unknown GUID must synthesise wav:guid_<text>, got {id:?}"
);
assert!(
id.contains("EFBEADDE-FECA-BEBA"),
"synthesised id must carry the canonical GUID text, got {id:?}"
);
}
fn extensible_wav_with_guid(guid: &[u8; 16], bits: u16) -> Vec<u8> {
let block_align = bits / 8;
let mut buf = Vec::new();
buf.extend_from_slice(b"RIFF");
buf.extend_from_slice(&0u32.to_le_bytes());
buf.extend_from_slice(b"WAVE");
buf.extend_from_slice(b"fmt ");
buf.extend_from_slice(&40u32.to_le_bytes());
buf.extend_from_slice(&FMT_EXTENSIBLE.to_le_bytes()); buf.extend_from_slice(&1u16.to_le_bytes()); buf.extend_from_slice(&44_100u32.to_le_bytes()); buf.extend_from_slice(&(44_100 * block_align as u32).to_le_bytes()); buf.extend_from_slice(&block_align.to_le_bytes()); buf.extend_from_slice(&bits.to_le_bytes()); buf.extend_from_slice(&22u16.to_le_bytes()); buf.extend_from_slice(&bits.to_le_bytes()); buf.extend_from_slice(&0x00004u32.to_le_bytes()); buf.extend_from_slice(guid); buf.extend_from_slice(b"data");
buf.extend_from_slice(&0u32.to_le_bytes());
buf
}
#[test]
fn waveformatex_tag_extracts_embedded_format_tag() {
assert_eq!(waveformatex_tag(&GUID_PCM), Some(0x0001));
assert_eq!(waveformatex_tag(&GUID_IEEE_FLOAT), Some(0x0003));
assert_eq!(waveformatex_tag(&GUID_ALAW), Some(0x0006));
assert_eq!(waveformatex_tag(&GUID_MULAW), Some(0x0007));
let mut mp3_guid = GUID_PCM;
mp3_guid[0] = 0x55;
mp3_guid[1] = 0x00;
assert_eq!(waveformatex_tag(&mp3_guid), Some(0x0055));
let bogus: [u8; 16] = [
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38,
0x9B, 0x72, ];
assert_eq!(waveformatex_tag(&bogus), None);
}
#[test]
fn extensible_template_guid_dispatches_through_legacy_tag() {
let buf = extensible_wav_with_guid(&GUID_IEEE_FLOAT, 32);
use std::io::Cursor;
let rs: Box<dyn ReadSeek> = Box::new(Cursor::new(buf));
let dmx = open_demuxer(rs, &oxideav_core::NullCodecResolver)
.expect("IEEE-float template GUID resolves");
let s = &dmx.streams()[0];
assert_eq!(s.params.codec_id.as_str(), "pcm_f32le");
let md: std::collections::HashMap<_, _> = dmx.metadata().iter().cloned().collect();
assert_eq!(
md.get("wav:fmt.subformat_tag").map(String::as_str),
Some("0x0003")
);
}
#[test]
fn extensible_template_guid_unmapped_tag_surfaces_tag() {
let mut mp3_guid = GUID_PCM;
mp3_guid[0] = 0x55; let buf = extensible_wav_with_guid(&mp3_guid, 16);
use std::io::Cursor;
let rs: Box<dyn ReadSeek> = Box::new(Cursor::new(buf));
let dmx = open_demuxer(rs, &oxideav_core::NullCodecResolver)
.expect("MP3 template GUID still parses");
let s = &dmx.streams()[0];
assert!(
s.params.codec_id.as_str().starts_with("wav:guid_"),
"unmapped tag must synthesise wav:guid_<text>, got {:?}",
s.params.codec_id.as_str()
);
let md: std::collections::HashMap<_, _> = dmx.metadata().iter().cloned().collect();
assert_eq!(
md.get("wav:fmt.subformat_tag").map(String::as_str),
Some("0x0055")
);
}
#[test]
fn extensible_short_cbsize_rejected() {
let mut buf = Vec::new();
buf.extend_from_slice(b"RIFF");
buf.extend_from_slice(&0u32.to_le_bytes());
buf.extend_from_slice(b"WAVE");
buf.extend_from_slice(b"fmt ");
buf.extend_from_slice(&40u32.to_le_bytes());
buf.extend_from_slice(&FMT_EXTENSIBLE.to_le_bytes());
buf.extend_from_slice(&1u16.to_le_bytes());
buf.extend_from_slice(&44_100u32.to_le_bytes());
buf.extend_from_slice(&88_200u32.to_le_bytes());
buf.extend_from_slice(&2u16.to_le_bytes());
buf.extend_from_slice(&16u16.to_le_bytes());
buf.extend_from_slice(&10u16.to_le_bytes()); buf.extend_from_slice(&[0u8; 20]); buf.extend_from_slice(b"data");
buf.extend_from_slice(&0u32.to_le_bytes());
use std::io::Cursor;
let rs: Box<dyn ReadSeek> = Box::new(Cursor::new(buf));
let err = open_demuxer(rs, &oxideav_core::NullCodecResolver).err();
assert!(
matches!(err, Some(Error::InvalidData(_))),
"short cbSize must yield Error::InvalidData, got {err:?}"
);
}
fn wav_with_bext(bext_body: &[u8]) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(b"RIFF");
buf.extend_from_slice(&0u32.to_le_bytes());
buf.extend_from_slice(b"WAVE");
buf.extend_from_slice(b"fmt ");
buf.extend_from_slice(&16u32.to_le_bytes());
buf.extend_from_slice(&FMT_PCM.to_le_bytes());
buf.extend_from_slice(&1u16.to_le_bytes()); buf.extend_from_slice(&8_000u32.to_le_bytes()); buf.extend_from_slice(&16_000u32.to_le_bytes()); buf.extend_from_slice(&2u16.to_le_bytes()); buf.extend_from_slice(&16u16.to_le_bytes()); buf.extend_from_slice(b"bext");
buf.extend_from_slice(&(bext_body.len() as u32).to_le_bytes());
buf.extend_from_slice(bext_body);
if bext_body.len() % 2 == 1 {
buf.push(0); }
buf.extend_from_slice(b"data");
buf.extend_from_slice(&0u32.to_le_bytes());
buf
}
#[allow(clippy::too_many_arguments)]
fn make_bext_v2(
description: &str,
originator: &str,
originator_ref: &str,
date: &str,
time: &str,
time_reference: u64,
umid: &[u8; 64],
loudness: [i16; 5],
coding_history: &str,
) -> Vec<u8> {
fn put_ascii(buf: &mut Vec<u8>, s: &str, width: usize) {
let bytes = s.as_bytes();
let n = bytes.len().min(width);
buf.extend_from_slice(&bytes[..n]);
buf.resize(buf.len() + (width - n), 0);
}
let mut b = Vec::new();
put_ascii(&mut b, description, 256);
put_ascii(&mut b, originator, 32);
put_ascii(&mut b, originator_ref, 32);
put_ascii(&mut b, date, 10);
put_ascii(&mut b, time, 8);
b.extend_from_slice(&(time_reference as u32).to_le_bytes()); b.extend_from_slice(&((time_reference >> 32) as u32).to_le_bytes()); b.extend_from_slice(&2u16.to_le_bytes()); b.extend_from_slice(umid);
for v in &loudness {
b.extend_from_slice(&v.to_le_bytes());
}
b.resize(BEXT_FIXED_LEN, 0); assert_eq!(b.len(), BEXT_FIXED_LEN);
b.extend_from_slice(coding_history.as_bytes());
b
}
#[test]
fn bext_loudness_formatting() {
assert_eq!(fmt_loudness(-2264), "-22.64");
assert_eq!(fmt_loudness(-2265), "-22.65");
assert_eq!(fmt_loudness(0), "0.00");
assert_eq!(fmt_loudness(2300), "23.00");
assert_eq!(fmt_loudness(-50), "-0.50"); assert_eq!(fmt_loudness(7), "0.07");
assert_eq!(fmt_loudness(-1), "-0.01");
}
#[test]
fn bext_v2_full_metadata() {
let mut umid = [0u8; 64];
umid[0] = 0x06;
umid[1] = 0x0a;
umid[63] = 0xff;
let body = make_bext_v2(
"Scene 1 take 3",
"OxideAV Recorder",
"USABC2400001",
"2026-05-23",
"14:30:00",
0x0000_0001_2345_6789,
&umid,
[-2305, 700, -120, -1850, -2010],
"A=PCM,F=48000,W=24,M=stereo,T=OxideAV\r\n",
);
let bytes = wav_with_bext(&body);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(
md.get("wav:bext.description"),
Some(&"Scene 1 take 3".to_string())
);
assert_eq!(
md.get("wav:bext.originator"),
Some(&"OxideAV Recorder".to_string())
);
assert_eq!(
md.get("wav:bext.originator_reference"),
Some(&"USABC2400001".to_string())
);
assert_eq!(
md.get("wav:bext.origination_date"),
Some(&"2026-05-23".to_string())
);
assert_eq!(
md.get("wav:bext.origination_time"),
Some(&"14:30:00".to_string())
);
assert_eq!(
md.get("wav:bext.time_reference"),
Some(&0x0000_0001_2345_6789u64.to_string())
);
assert_eq!(md.get("wav:bext.version"), Some(&"2".to_string()));
assert_eq!(
md.get("wav:bext.loudness_value"),
Some(&"-23.05".to_string())
);
assert_eq!(md.get("wav:bext.loudness_range"), Some(&"7.00".to_string()));
assert_eq!(
md.get("wav:bext.max_true_peak_level"),
Some(&"-1.20".to_string())
);
assert_eq!(
md.get("wav:bext.max_momentary_loudness"),
Some(&"-18.50".to_string())
);
assert_eq!(
md.get("wav:bext.max_short_term_loudness"),
Some(&"-20.10".to_string())
);
let umid_hex = md.get("wav:bext.umid").expect("umid present");
assert!(umid_hex.starts_with("060a"), "umid hex {umid_hex:?}");
assert!(umid_hex.ends_with("ff"), "umid hex {umid_hex:?}");
assert_eq!(umid_hex.len(), 128); assert_eq!(
md.get("wav:bext.coding_history"),
Some(&"A=PCM,F=48000,W=24,M=stereo,T=OxideAV".to_string())
);
}
#[test]
fn bext_v0_omits_umid_and_loudness() {
let mut body = vec![0u8; BEXT_FIXED_LEN];
let desc = b"Field recording";
body[..desc.len()].copy_from_slice(desc);
body[338..342].copy_from_slice(&12_345u32.to_le_bytes());
let bytes = wav_with_bext(&body);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(
md.get("wav:bext.description"),
Some(&"Field recording".to_string())
);
assert_eq!(md.get("wav:bext.version"), Some(&"0".to_string()));
assert_eq!(
md.get("wav:bext.time_reference"),
Some(&"12345".to_string())
);
assert!(!md.contains_key("wav:bext.umid"));
assert!(!md.contains_key("wav:bext.loudness_value"));
assert!(!md.contains_key("wav:bext.max_true_peak_level"));
}
#[test]
fn bext_truncated_is_skipped() {
let body = vec![0u8; 100]; let bytes = wav_with_bext(&body);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert!(md.keys().all(|k| !k.starts_with("wav:bext.")));
assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
}
fn wav_with_cue_and_adtl(cue_body: &[u8], adtl_body: Option<&[u8]>) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(b"RIFF");
buf.extend_from_slice(&0u32.to_le_bytes());
buf.extend_from_slice(b"WAVE");
buf.extend_from_slice(b"fmt ");
buf.extend_from_slice(&16u32.to_le_bytes());
buf.extend_from_slice(&FMT_PCM.to_le_bytes());
buf.extend_from_slice(&1u16.to_le_bytes());
buf.extend_from_slice(&8_000u32.to_le_bytes());
buf.extend_from_slice(&16_000u32.to_le_bytes());
buf.extend_from_slice(&2u16.to_le_bytes());
buf.extend_from_slice(&16u16.to_le_bytes());
buf.extend_from_slice(b"cue ");
buf.extend_from_slice(&(cue_body.len() as u32).to_le_bytes());
buf.extend_from_slice(cue_body);
if cue_body.len() % 2 == 1 {
buf.push(0);
}
if let Some(adtl) = adtl_body {
buf.extend_from_slice(b"LIST");
buf.extend_from_slice(&((adtl.len() + 4) as u32).to_le_bytes());
buf.extend_from_slice(b"adtl");
buf.extend_from_slice(adtl);
if (adtl.len() + 4) % 2 == 1 {
buf.push(0);
}
}
buf.extend_from_slice(b"data");
buf.extend_from_slice(&0u32.to_le_bytes());
buf
}
fn cue_point(
dw_name: u32,
dw_position: u32,
fcc_chunk: &[u8; 4],
dw_chunk_start: u32,
dw_block_start: u32,
dw_sample_offset: u32,
) -> Vec<u8> {
let mut b = Vec::with_capacity(24);
b.extend_from_slice(&dw_name.to_le_bytes());
b.extend_from_slice(&dw_position.to_le_bytes());
b.extend_from_slice(fcc_chunk);
b.extend_from_slice(&dw_chunk_start.to_le_bytes());
b.extend_from_slice(&dw_block_start.to_le_bytes());
b.extend_from_slice(&dw_sample_offset.to_le_bytes());
b
}
fn adtl_text_subchunk(id: &[u8; 4], dw_name: u32, text: &str) -> Vec<u8> {
let mut b = Vec::new();
b.extend_from_slice(id);
let body_len = 4 + text.len() + 1;
b.extend_from_slice(&(body_len as u32).to_le_bytes());
b.extend_from_slice(&dw_name.to_le_bytes());
b.extend_from_slice(text.as_bytes());
b.push(0);
if body_len % 2 == 1 {
b.push(0);
}
b
}
#[test]
fn cue_and_adtl_full_metadata() {
let mut cue_body = Vec::new();
cue_body.extend_from_slice(&2u32.to_le_bytes()); cue_body.extend(cue_point(1, 0, b"data", 0, 0, 0));
cue_body.extend(cue_point(2, 12_345, b"data", 0, 0, 12_345));
let mut adtl = Vec::new();
adtl.extend(adtl_text_subchunk(b"labl", 1, "Intro"));
adtl.extend(adtl_text_subchunk(b"note", 1, "Fade-in"));
adtl.extend(adtl_text_subchunk(b"labl", 2, "Verse"));
adtl.extend(adtl_text_subchunk(b"note", 2, "Vocal entry"));
let bytes = wav_with_cue_and_adtl(&cue_body, Some(&adtl));
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:cue.count"), Some(&"2".to_string()));
assert_eq!(md.get("wav:cue.1.position"), Some(&"0".to_string()));
assert_eq!(md.get("wav:cue.1.fcc_chunk"), Some(&"data".to_string()));
assert_eq!(md.get("wav:cue.1.chunk_start"), Some(&"0".to_string()));
assert_eq!(md.get("wav:cue.1.block_start"), Some(&"0".to_string()));
assert_eq!(md.get("wav:cue.1.sample_offset"), Some(&"0".to_string()));
assert_eq!(md.get("wav:cue.2.position"), Some(&"12345".to_string()));
assert_eq!(
md.get("wav:cue.2.sample_offset"),
Some(&"12345".to_string())
);
assert_eq!(md.get("wav:adtl.labl.1"), Some(&"Intro".to_string()));
assert_eq!(md.get("wav:adtl.note.1"), Some(&"Fade-in".to_string()));
assert_eq!(md.get("wav:adtl.labl.2"), Some(&"Verse".to_string()));
assert_eq!(md.get("wav:adtl.note.2"), Some(&"Vocal entry".to_string()));
}
#[test]
fn adtl_ltxt_segment_metadata() {
let mut cue_body = Vec::new();
cue_body.extend_from_slice(&1u32.to_le_bytes());
cue_body.extend(cue_point(7, 1000, b"data", 0, 0, 1000));
let mut ltxt_body = Vec::new();
ltxt_body.extend_from_slice(&7u32.to_le_bytes()); ltxt_body.extend_from_slice(&4410u32.to_le_bytes()); ltxt_body.extend_from_slice(b"scrp"); ltxt_body.extend_from_slice(&44u16.to_le_bytes()); ltxt_body.extend_from_slice(&9u16.to_le_bytes()); ltxt_body.extend_from_slice(&2u16.to_le_bytes()); ltxt_body.extend_from_slice(&1252u16.to_le_bytes()); ltxt_body.extend_from_slice(b"Hello world");
ltxt_body.push(0);
let mut adtl = Vec::new();
adtl.extend_from_slice(b"ltxt");
adtl.extend_from_slice(&(ltxt_body.len() as u32).to_le_bytes());
adtl.extend_from_slice(<xt_body);
if ltxt_body.len() % 2 == 1 {
adtl.push(0);
}
let bytes = wav_with_cue_and_adtl(&cue_body, Some(&adtl));
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:adtl.ltxt.7.length"), Some(&"4410".to_string()));
assert_eq!(md.get("wav:adtl.ltxt.7.purpose"), Some(&"scrp".to_string()));
assert_eq!(
md.get("wav:adtl.ltxt.7.text"),
Some(&"Hello world".to_string())
);
assert_eq!(md.get("wav:adtl.ltxt.7.country"), Some(&"44".to_string()));
assert_eq!(
md.get("wav:adtl.ltxt.7.country_name"),
Some(&"United Kingdom".to_string())
);
assert_eq!(md.get("wav:adtl.ltxt.7.language"), Some(&"9".to_string()));
assert_eq!(md.get("wav:adtl.ltxt.7.dialect"), Some(&"2".to_string()));
assert_eq!(
md.get("wav:adtl.ltxt.7.language_name"),
Some(&"UK English".to_string())
);
assert_eq!(
md.get("wav:adtl.ltxt.7.code_page"),
Some(&"1252".to_string())
);
}
#[test]
fn adtl_ltxt_zero_locale_fields_surface() {
let mut cue_body = Vec::new();
cue_body.extend_from_slice(&1u32.to_le_bytes());
cue_body.extend(cue_point(3, 0, b"data", 0, 0, 0));
let mut ltxt_body = Vec::new();
ltxt_body.extend_from_slice(&3u32.to_le_bytes()); ltxt_body.extend_from_slice(&100u32.to_le_bytes()); ltxt_body.extend_from_slice(b"capt"); ltxt_body.extend_from_slice(&[0u8; 8]);
let mut adtl = Vec::new();
adtl.extend_from_slice(b"ltxt");
adtl.extend_from_slice(&(ltxt_body.len() as u32).to_le_bytes());
adtl.extend_from_slice(<xt_body);
let bytes = wav_with_cue_and_adtl(&cue_body, Some(&adtl));
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:adtl.ltxt.3.country"), Some(&"0".to_string()));
assert_eq!(
md.get("wav:adtl.ltxt.3.country_name"),
Some(&"None".to_string())
);
assert_eq!(md.get("wav:adtl.ltxt.3.language"), Some(&"0".to_string()));
assert_eq!(md.get("wav:adtl.ltxt.3.dialect"), Some(&"0".to_string()));
assert_eq!(
md.get("wav:adtl.ltxt.3.language_name"),
Some(&"None".to_string())
);
assert_eq!(md.get("wav:adtl.ltxt.3.code_page"), Some(&"0".to_string()));
assert_eq!(md.get("wav:adtl.ltxt.3.text"), None);
}
#[test]
fn adtl_file_subchunk_metadata() {
let mut cue_body = Vec::new();
cue_body.extend_from_slice(&1u32.to_le_bytes());
cue_body.extend(cue_point(5, 0, b"data", 0, 0, 0));
let mut file_body = Vec::new();
file_body.extend_from_slice(&5u32.to_le_bytes()); file_body.extend_from_slice(b"RDIB"); file_body.extend_from_slice(&[0xAAu8; 11]);
let mut adtl = Vec::new();
adtl.extend_from_slice(b"file");
adtl.extend_from_slice(&(file_body.len() as u32).to_le_bytes());
adtl.extend_from_slice(&file_body);
if file_body.len() % 2 == 1 {
adtl.push(0);
}
let mut file2_body = Vec::new();
file2_body.extend_from_slice(&6u32.to_le_bytes()); file2_body.extend_from_slice(&0u32.to_le_bytes()); adtl.extend_from_slice(b"file");
adtl.extend_from_slice(&(file2_body.len() as u32).to_le_bytes());
adtl.extend_from_slice(&file2_body);
let bytes = wav_with_cue_and_adtl(&cue_body, Some(&adtl));
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(
md.get("wav:adtl.file.5.med_type"),
Some(&"RDIB".to_string())
);
assert_eq!(md.get("wav:adtl.file.5.body_len"), Some(&"11".to_string()));
assert_eq!(md.get("wav:adtl.file.6.med_type"), Some(&"0".to_string()));
assert_eq!(md.get("wav:adtl.file.6.body_len"), Some(&"0".to_string()));
}
#[test]
fn adtl_file_truncated_is_skipped() {
let cue_body = 0u32.to_le_bytes().to_vec();
let mut adtl = Vec::new();
adtl.extend_from_slice(b"file");
adtl.extend_from_slice(&6u32.to_le_bytes()); adtl.extend_from_slice(&[0u8; 6]);
let bytes = wav_with_cue_and_adtl(&cue_body, Some(&adtl));
let dmx = open_demux_from_bytes(bytes);
assert!(dmx
.metadata()
.iter()
.all(|(k, _)| !k.starts_with("wav:adtl.file.")));
assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
}
#[test]
fn cue_truncated_count_is_clamped() {
let mut cue_body = Vec::new();
cue_body.extend_from_slice(&5u32.to_le_bytes());
cue_body.extend(cue_point(42, 100, b"data", 0, 0, 100));
let bytes = wav_with_cue_and_adtl(&cue_body, None);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:cue.count"), Some(&"1".to_string()));
assert_eq!(md.get("wav:cue.42.position"), Some(&"100".to_string()));
assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
}
#[test]
fn adtl_without_cue_still_surfaces() {
let mut adtl = Vec::new();
adtl.extend(adtl_text_subchunk(b"labl", 99, "Orphan label"));
let cue_body = 0u32.to_le_bytes().to_vec();
let bytes = wav_with_cue_and_adtl(&cue_body, Some(&adtl));
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:cue.count"), Some(&"0".to_string()));
assert_eq!(
md.get("wav:adtl.labl.99"),
Some(&"Orphan label".to_string())
);
}
fn wav_with_smpl_and_inst(smpl_body: Option<&[u8]>, inst_body: Option<&[u8]>) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(b"RIFF");
buf.extend_from_slice(&0u32.to_le_bytes());
buf.extend_from_slice(b"WAVE");
buf.extend_from_slice(b"fmt ");
buf.extend_from_slice(&16u32.to_le_bytes());
buf.extend_from_slice(&FMT_PCM.to_le_bytes());
buf.extend_from_slice(&1u16.to_le_bytes());
buf.extend_from_slice(&8_000u32.to_le_bytes());
buf.extend_from_slice(&16_000u32.to_le_bytes());
buf.extend_from_slice(&2u16.to_le_bytes());
buf.extend_from_slice(&16u16.to_le_bytes());
if let Some(smpl) = smpl_body {
buf.extend_from_slice(b"smpl");
buf.extend_from_slice(&(smpl.len() as u32).to_le_bytes());
buf.extend_from_slice(smpl);
if smpl.len() % 2 == 1 {
buf.push(0);
}
}
if let Some(inst) = inst_body {
buf.extend_from_slice(b"inst");
buf.extend_from_slice(&(inst.len() as u32).to_le_bytes());
buf.extend_from_slice(inst);
if inst.len() % 2 == 1 {
buf.push(0);
}
}
buf.extend_from_slice(b"data");
buf.extend_from_slice(&0u32.to_le_bytes());
buf
}
#[allow(clippy::too_many_arguments)]
fn smpl_body(
manufacturer: u32,
product: u32,
sample_period: u32,
midi_unity_note: u32,
midi_pitch_fraction: u32,
smpte_format: u32,
smpte_offset: u32,
c_sample_loops_claimed: u32,
cb_sampler_data: u32,
loops: &[(u32, u32, u32, u32, u32, u32)],
) -> Vec<u8> {
let mut b = Vec::new();
for v in [
manufacturer,
product,
sample_period,
midi_unity_note,
midi_pitch_fraction,
smpte_format,
smpte_offset,
c_sample_loops_claimed,
cb_sampler_data,
] {
b.extend_from_slice(&v.to_le_bytes());
}
for &(id, ty, start, end, frac, count) in loops {
for v in [id, ty, start, end, frac, count] {
b.extend_from_slice(&v.to_le_bytes());
}
}
b
}
#[test]
fn smpl_full_metadata() {
let body = smpl_body(
0x1234, 0xDEAD_BEEF, 22_675, 60, 0x8000_0000, 30, 0x01_02_03_04, 1, 0, &[(7, 0, 0, 1000, 0, 0)], );
let bytes = wav_with_smpl_and_inst(Some(&body), None);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:smpl.manufacturer"), Some(&"4660".to_string()));
assert_eq!(md.get("wav:smpl.product"), Some(&"3735928559".to_string()));
assert_eq!(md.get("wav:smpl.sample_period"), Some(&"22675".to_string()));
assert_eq!(md.get("wav:smpl.midi_unity_note"), Some(&"60".to_string()));
assert_eq!(
md.get("wav:smpl.midi_pitch_fraction"),
Some(&"2147483648".to_string())
);
assert_eq!(md.get("wav:smpl.smpte_format"), Some(&"30".to_string()));
assert_eq!(
md.get("wav:smpl.smpte_offset"),
Some(&"01:02:03:04".to_string())
);
assert_eq!(md.get("wav:smpl.sampler_data_len"), Some(&"0".to_string()));
assert_eq!(md.get("wav:smpl.num_sample_loops"), Some(&"1".to_string()));
assert_eq!(
md.get("wav:smpl.loop.0.cue_point_id"),
Some(&"7".to_string())
);
assert_eq!(md.get("wav:smpl.loop.0.type"), Some(&"0".to_string()));
assert_eq!(md.get("wav:smpl.loop.0.start"), Some(&"0".to_string()));
assert_eq!(md.get("wav:smpl.loop.0.end"), Some(&"1000".to_string()));
assert_eq!(md.get("wav:smpl.loop.0.fraction"), Some(&"0".to_string()));
assert_eq!(md.get("wav:smpl.loop.0.play_count"), Some(&"0".to_string()));
assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
}
#[test]
fn smpl_loop_count_clamped_to_body() {
let body = smpl_body(
0,
0,
0,
60,
0,
0,
0,
5,
0,
&[(1, 0, 0, 100, 0, 0)],
);
let bytes = wav_with_smpl_and_inst(Some(&body), None);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:smpl.num_sample_loops"), Some(&"1".to_string()));
assert!(md.contains_key("wav:smpl.loop.0.cue_point_id"));
assert!(!md.contains_key("wav:smpl.loop.1.cue_point_id"));
assert!(!md.contains_key("wav:smpl.loop.4.cue_point_id"));
}
#[test]
fn smpl_truncated_is_skipped() {
let body = vec![0u8; 20]; let bytes = wav_with_smpl_and_inst(Some(&body), None);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert!(md.keys().all(|k| !k.starts_with("wav:smpl.")));
assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
}
#[test]
fn inst_full_metadata() {
let body: Vec<u8> = vec![60, 0xFD, 0xFA, 36, 96, 1, 127];
let bytes = wav_with_smpl_and_inst(None, Some(&body));
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:inst.unshifted_note"), Some(&"60".to_string()));
assert_eq!(md.get("wav:inst.fine_tune"), Some(&"-3".to_string()));
assert_eq!(md.get("wav:inst.gain"), Some(&"-6".to_string()));
assert_eq!(md.get("wav:inst.low_note"), Some(&"36".to_string()));
assert_eq!(md.get("wav:inst.high_note"), Some(&"96".to_string()));
assert_eq!(md.get("wav:inst.low_velocity"), Some(&"1".to_string()));
assert_eq!(md.get("wav:inst.high_velocity"), Some(&"127".to_string()));
assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
}
#[test]
fn inst_truncated_is_skipped() {
let body = vec![0u8; 5]; let bytes = wav_with_smpl_and_inst(None, Some(&body));
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert!(md.keys().all(|k| !k.starts_with("wav:inst.")));
assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
}
#[test]
fn smpl_and_inst_coexist_with_padding() {
let smpl = smpl_body(0, 0, 0, 64, 0, 0, 0, 0, 0, &[]);
let inst: Vec<u8> = vec![64, 0, 0, 0, 127, 1, 127]; let bytes = wav_with_smpl_and_inst(Some(&smpl), Some(&inst));
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:smpl.midi_unity_note"), Some(&"64".to_string()));
assert_eq!(md.get("wav:inst.unshifted_note"), Some(&"64".to_string()));
assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
}
fn wav_with_acid(acid_body: &[u8]) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(b"RIFF");
buf.extend_from_slice(&0u32.to_le_bytes());
buf.extend_from_slice(b"WAVE");
buf.extend_from_slice(b"fmt ");
buf.extend_from_slice(&16u32.to_le_bytes());
buf.extend_from_slice(&FMT_PCM.to_le_bytes());
buf.extend_from_slice(&1u16.to_le_bytes());
buf.extend_from_slice(&8_000u32.to_le_bytes());
buf.extend_from_slice(&16_000u32.to_le_bytes());
buf.extend_from_slice(&2u16.to_le_bytes());
buf.extend_from_slice(&16u16.to_le_bytes());
buf.extend_from_slice(b"acid");
buf.extend_from_slice(&(acid_body.len() as u32).to_le_bytes());
buf.extend_from_slice(acid_body);
if acid_body.len() % 2 == 1 {
buf.push(0);
}
buf.extend_from_slice(b"data");
buf.extend_from_slice(&0u32.to_le_bytes());
buf
}
#[test]
fn acid_chunk_byte_layout_pinned() {
let acid = AcidChunk {
flags: ACID_FLAG_ROOT_NOTE_SET | ACID_FLAG_STRETCH,
root_note: 57, reserved: [0x80, 0x00, 0x01, 0x02, 0x03, 0x04],
num_beats: 16,
meter: 4,
tempo: 120.5,
};
let bytes = acid.to_bytes();
#[rustfmt::skip]
let expected: [u8; 24] = [
0x06, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x00, 0x01, 0x02, 0x03, 0x04, 0x10, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF1, 0x42, ];
assert_eq!(bytes, expected);
assert_eq!(AcidChunk::parse(&bytes), Some(acid));
}
#[test]
fn acid_full_metadata() {
let acid = AcidChunk {
flags: ACID_FLAG_ONE_SHOT | ACID_FLAG_ROOT_NOTE_SET | ACID_FLAG_HIGH_OCTAVE,
root_note: 60, reserved: [0; 6],
num_beats: 32,
meter: 4,
tempo: 95.0,
};
let bytes = wav_with_acid(&acid.to_bytes());
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:acid.flags"), Some(&"0x00000013".to_string()));
assert_eq!(md.get("wav:acid.one_shot"), Some(&"1".to_string()));
assert_eq!(md.get("wav:acid.root_note_set"), Some(&"1".to_string()));
assert_eq!(md.get("wav:acid.stretch"), Some(&"0".to_string()));
assert_eq!(md.get("wav:acid.disk_based"), Some(&"0".to_string()));
assert_eq!(md.get("wav:acid.high_octave"), Some(&"1".to_string()));
assert_eq!(md.get("wav:acid.root_note"), Some(&"60".to_string()));
assert_eq!(
md.get("wav:acid.root_note_name"),
Some(&"High C".to_string())
);
assert_eq!(md.get("wav:acid.num_beats"), Some(&"32".to_string()));
assert_eq!(md.get("wav:acid.meter"), Some(&"4".to_string()));
assert_eq!(md.get("wav:acid.tempo"), Some(&"95".to_string()));
assert_eq!(md.get("wav:acid.reserved"), None);
assert_eq!(md.get("wav:acid.body_len"), None);
assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
}
#[test]
fn acid_out_of_table_root_note_and_extras() {
let acid = AcidChunk {
flags: 0,
root_note: 0,
reserved: [0xAA, 0, 0, 0, 0, 0xBB],
num_beats: 8,
meter: 4,
tempo: 133.25,
};
assert_eq!(acid.root_note_name(), None);
let mut body = acid.to_bytes().to_vec();
body.extend_from_slice(&[0xEE, 0xFF]); let bytes = wav_with_acid(&body);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:acid.root_note"), Some(&"0".to_string()));
assert_eq!(md.get("wav:acid.root_note_name"), None);
assert_eq!(md.get("wav:acid.tempo"), Some(&"133.25".to_string()));
assert_eq!(
md.get("wav:acid.reserved"),
Some(&"AA00000000BB".to_string())
);
assert_eq!(md.get("wav:acid.body_len"), Some(&"26".to_string()));
}
#[test]
fn acid_truncated_is_skipped() {
let bytes = wav_with_acid(&[0u8; 23]);
let dmx = open_demux_from_bytes(bytes);
assert!(!dmx
.metadata()
.iter()
.any(|(k, _)| k.starts_with("wav:acid")));
assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
}
#[test]
fn acid_round_trip() {
let acid = AcidChunk {
flags: ACID_FLAG_ROOT_NOTE_SET,
root_note: 50, reserved: [0; 6],
num_beats: 64,
meter: 4,
tempo: 174.0,
};
let payload: Vec<u8> = (0..400u32).flat_map(|i| (i as i16).to_le_bytes()).collect();
let stream = make_stream(SampleFormat::S16, 1, 44_100);
let opts = WavMuxOptions::default().with_acid(acid);
let bytes = mux_to_bytes(&stream, &payload, opts, "acid-rt");
let mut chunk = b"acid".to_vec();
chunk.extend_from_slice(&24u32.to_le_bytes());
chunk.extend_from_slice(&acid.to_bytes());
assert!(bytes.windows(chunk.len()).any(|w| w == &chunk[..]));
let mut dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:acid.root_note"), Some(&"50".to_string()));
assert_eq!(md.get("wav:acid.root_note_name"), Some(&"D".to_string()));
assert_eq!(md.get("wav:acid.num_beats"), Some(&"64".to_string()));
assert_eq!(md.get("wav:acid.tempo"), Some(&"174".to_string()));
let mut out = Vec::new();
loop {
match dmx.next_packet() {
Ok(p) => out.extend_from_slice(&p.data),
Err(Error::Eof) => break,
Err(e) => panic!("demux error: {e}"),
}
}
assert_eq!(out, payload);
}
#[test]
fn acid_typed_accessor() {
let acid = AcidChunk {
flags: ACID_FLAG_ONE_SHOT | ACID_FLAG_DISK_BASED,
root_note: 71, reserved: [1, 2, 3, 4, 5, 6],
num_beats: 4,
meter: 3,
tempo: 60.0,
};
let bytes = wav_with_acid(&acid.to_bytes());
use std::io::Cursor;
let dmx = open_wav_demuxer(Box::new(Cursor::new(bytes))).unwrap();
assert_eq!(dmx.acid(), Some(&acid));
let got = dmx.acid().unwrap();
assert!(got.one_shot() && got.disk_based());
assert!(!got.root_note_set() && !got.stretch() && !got.high_octave());
assert_eq!(got.root_note_name(), Some("High B"));
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(
md.get("wav:acid.reserved"),
Some(&"010203040506".to_string())
);
let plain = wav_with_smpl_and_inst(None, None);
let dmx = open_wav_demuxer(Box::new(Cursor::new(plain))).unwrap();
assert_eq!(dmx.acid(), None);
}
fn wav_with_plst(plst_body: &[u8]) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(b"RIFF");
buf.extend_from_slice(&0u32.to_le_bytes());
buf.extend_from_slice(b"WAVE");
buf.extend_from_slice(b"fmt ");
buf.extend_from_slice(&16u32.to_le_bytes());
buf.extend_from_slice(&FMT_PCM.to_le_bytes());
buf.extend_from_slice(&1u16.to_le_bytes());
buf.extend_from_slice(&8_000u32.to_le_bytes());
buf.extend_from_slice(&16_000u32.to_le_bytes());
buf.extend_from_slice(&2u16.to_le_bytes());
buf.extend_from_slice(&16u16.to_le_bytes());
buf.extend_from_slice(b"plst");
buf.extend_from_slice(&(plst_body.len() as u32).to_le_bytes());
buf.extend_from_slice(plst_body);
if plst_body.len() % 2 == 1 {
buf.push(0);
}
buf.extend_from_slice(b"data");
buf.extend_from_slice(&0u32.to_le_bytes());
buf
}
fn plst_segment(dw_name: u32, dw_length: u32, dw_loops: u32) -> Vec<u8> {
let mut b = Vec::with_capacity(12);
b.extend_from_slice(&dw_name.to_le_bytes());
b.extend_from_slice(&dw_length.to_le_bytes());
b.extend_from_slice(&dw_loops.to_le_bytes());
b
}
#[test]
fn plst_full_metadata() {
let mut plst_body = Vec::new();
plst_body.extend_from_slice(&3u32.to_le_bytes()); plst_body.extend(plst_segment(1, 4410, 1)); plst_body.extend(plst_segment(2, 8820, 2)); plst_body.extend(plst_segment(1, 4410, 1));
let bytes = wav_with_plst(&plst_body);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:plst.count"), Some(&"3".to_string()));
assert_eq!(md.get("wav:plst.0.cue_id"), Some(&"1".to_string()));
assert_eq!(md.get("wav:plst.0.length"), Some(&"4410".to_string()));
assert_eq!(md.get("wav:plst.0.loops"), Some(&"1".to_string()));
assert_eq!(md.get("wav:plst.1.cue_id"), Some(&"2".to_string()));
assert_eq!(md.get("wav:plst.1.length"), Some(&"8820".to_string()));
assert_eq!(md.get("wav:plst.1.loops"), Some(&"2".to_string()));
assert_eq!(md.get("wav:plst.2.cue_id"), Some(&"1".to_string()));
assert_eq!(md.get("wav:plst.2.length"), Some(&"4410".to_string()));
assert_eq!(md.get("wav:plst.2.loops"), Some(&"1".to_string()));
}
#[test]
fn plst_truncated_count_is_clamped() {
let mut plst_body = Vec::new();
plst_body.extend_from_slice(&10u32.to_le_bytes());
plst_body.extend(plst_segment(42, 1000, 1));
let bytes = wav_with_plst(&plst_body);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:plst.count"), Some(&"1".to_string()));
assert_eq!(md.get("wav:plst.0.cue_id"), Some(&"42".to_string()));
assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
}
#[test]
fn plst_truncated_header_is_opaque() {
let plst_body = vec![0u8, 0]; let bytes = wav_with_plst(&plst_body);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert!(md.keys().all(|k| !k.starts_with("wav:plst.")));
assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
}
#[test]
fn plst_zero_segments() {
let plst_body = 0u32.to_le_bytes().to_vec();
let bytes = wav_with_plst(&plst_body);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:plst.count"), Some(&"0".to_string()));
assert!(md
.keys()
.all(|k| !k.starts_with("wav:plst.") || k == "wav:plst.count"));
}
#[test]
fn plst_odd_body_padding() {
let mut plst_body = Vec::new();
plst_body.extend_from_slice(&1u32.to_le_bytes());
plst_body.extend(plst_segment(5, 100, 1));
plst_body.push(0xAA);
let bytes = wav_with_plst(&plst_body);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:plst.count"), Some(&"1".to_string()));
assert_eq!(md.get("wav:plst.0.cue_id"), Some(&"5".to_string()));
assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
}
fn wav_with_fact(fact_body: &[u8], data_payload: &[u8]) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(b"RIFF");
buf.extend_from_slice(&0u32.to_le_bytes());
buf.extend_from_slice(b"WAVE");
buf.extend_from_slice(b"fmt ");
buf.extend_from_slice(&16u32.to_le_bytes());
buf.extend_from_slice(&FMT_PCM.to_le_bytes());
buf.extend_from_slice(&1u16.to_le_bytes());
buf.extend_from_slice(&8_000u32.to_le_bytes());
buf.extend_from_slice(&16_000u32.to_le_bytes());
buf.extend_from_slice(&2u16.to_le_bytes());
buf.extend_from_slice(&16u16.to_le_bytes());
buf.extend_from_slice(b"fact");
buf.extend_from_slice(&(fact_body.len() as u32).to_le_bytes());
buf.extend_from_slice(fact_body);
if fact_body.len() % 2 == 1 {
buf.push(0);
}
buf.extend_from_slice(b"data");
buf.extend_from_slice(&(data_payload.len() as u32).to_le_bytes());
buf.extend_from_slice(data_payload);
if data_payload.len() % 2 == 1 {
buf.push(0);
}
buf
}
#[test]
fn fact_minimum_body_matches_data() {
let payload: Vec<u8> = (0..200u32).map(|i| (i & 0xFF) as u8).collect();
let fact_body = 100u32.to_le_bytes().to_vec();
let bytes = wav_with_fact(&fact_body, &payload);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:fact.sample_count"), Some(&"100".to_string()));
assert!(!md.contains_key("wav:fact.mismatch"));
assert!(!md.contains_key("wav:fact.body_len"));
assert_eq!(dmx.streams()[0].duration, Some(100));
}
#[test]
fn fact_mismatch_surfaces_diagnostic_and_overrides_duration() {
let payload: Vec<u8> = (0..200u32).map(|i| (i & 0xFF) as u8).collect();
let fact_body = 50u32.to_le_bytes().to_vec();
let bytes = wav_with_fact(&fact_body, &payload);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:fact.sample_count"), Some(&"50".to_string()));
assert_eq!(
md.get("wav:fact.mismatch"),
Some(&"block_samples=100 fact_samples=50".to_string())
);
assert_eq!(dmx.streams()[0].duration, Some(50));
}
#[test]
fn fact_extension_bytes_preserved_in_body_len() {
let payload: Vec<u8> = (0..200u32).map(|i| (i & 0xFF) as u8).collect();
let mut fact_body = 100u32.to_le_bytes().to_vec();
fact_body.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03, 0x04]);
let bytes = wav_with_fact(&fact_body, &payload);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:fact.sample_count"), Some(&"100".to_string()));
assert_eq!(md.get("wav:fact.body_len"), Some(&"12".to_string()));
assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
}
#[test]
fn fact_truncated_body_is_opaque() {
let payload: Vec<u8> = vec![0u8, 0u8];
let fact_body = vec![0u8, 0u8]; let bytes = wav_with_fact(&fact_body, &payload);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert!(md.keys().all(|k| !k.starts_with("wav:fact.")));
assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
}
#[test]
fn fact_odd_body_padding() {
let payload: Vec<u8> = vec![0xAA, 0x55];
let mut fact_body = 1u32.to_le_bytes().to_vec();
fact_body.push(0x42);
let bytes = wav_with_fact(&fact_body, &payload);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:fact.sample_count"), Some(&"1".to_string()));
assert_eq!(md.get("wav:fact.body_len"), Some(&"5".to_string()));
assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
}
#[test]
fn fact_chunk_round_trip_alaw_mono() {
let payload: Vec<u8> = (0..=255u8).collect(); let stream = make_g711_stream("pcm_alaw", 1, 8_000);
let bytes = mux_to_bytes(&stream, &payload, WavMuxOptions::default(), "alaw-fact");
assert!(
find_chunk(&bytes, b"fact").is_some(),
"muxer must emit fact chunk for non-PCM wFormatTag"
);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:fact.sample_count"), Some(&"256".to_string()));
assert!(!md.contains_key("wav:fact.mismatch"));
assert_eq!(dmx.streams()[0].duration, Some(256));
}
#[test]
fn fact_chunk_not_emitted_for_pcm() {
let samples: Vec<i16> = (0..100).map(|i| (i * 100) as i16).collect();
let mut payload = Vec::with_capacity(samples.len() * 2);
for s in &samples {
payload.extend_from_slice(&s.to_le_bytes());
}
let stream = make_stream(SampleFormat::S16, 1, 8_000);
let bytes = mux_to_bytes(&stream, &payload, WavMuxOptions::default(), "pcm-no-fact");
assert!(
find_chunk(&bytes, b"fact").is_none(),
"PCM muxer output must not carry a fact chunk"
);
}
#[test]
fn fact_chunk_round_trip_extensible() {
let samples: Vec<i16> = (0..200).map(|i| (i * 50) as i16).collect();
let mut payload = Vec::with_capacity(samples.len() * 2);
for s in &samples {
payload.extend_from_slice(&s.to_le_bytes());
}
let stream = make_stream(SampleFormat::S16, 1, 8_000);
let opts = WavMuxOptions::default().with_extensible(0x4); let bytes = mux_to_bytes(&stream, &payload, opts, "ext-fact");
assert!(
find_chunk(&bytes, b"fact").is_some(),
"EXTENSIBLE muxer output must carry a fact chunk"
);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:fact.sample_count"), Some(&"200".to_string()));
assert!(!md.contains_key("wav:fact.mismatch"));
}
fn wav_with_ixml(ixml_body: &[u8]) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(b"RIFF");
buf.extend_from_slice(&0u32.to_le_bytes());
buf.extend_from_slice(b"WAVE");
buf.extend_from_slice(b"fmt ");
buf.extend_from_slice(&16u32.to_le_bytes());
buf.extend_from_slice(&FMT_PCM.to_le_bytes());
buf.extend_from_slice(&1u16.to_le_bytes());
buf.extend_from_slice(&8_000u32.to_le_bytes());
buf.extend_from_slice(&16_000u32.to_le_bytes());
buf.extend_from_slice(&2u16.to_le_bytes());
buf.extend_from_slice(&16u16.to_le_bytes());
buf.extend_from_slice(b"iXML");
buf.extend_from_slice(&(ixml_body.len() as u32).to_le_bytes());
buf.extend_from_slice(ixml_body);
if ixml_body.len() % 2 == 1 {
buf.push(0);
}
buf.extend_from_slice(b"data");
buf.extend_from_slice(&0u32.to_le_bytes());
buf
}
#[test]
fn ixml_canonical_document_round_trips() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<BWFXML>
<IXML_VERSION>2.10</IXML_VERSION>
<PROJECT>OxideAV Round 205</PROJECT>
<SCENE>scn-001</SCENE>
<TAKE>1</TAKE>
<NOTE>iXML canonical fixture</NOTE>
<TRACK_LIST>
<TRACK_COUNT>1</TRACK_COUNT>
<TRACK>
<CHANNEL_INDEX>1</CHANNEL_INDEX>
<NAME>Boom</NAME>
<FUNCTION>Dialog</FUNCTION>
</TRACK>
</TRACK_LIST>
</BWFXML>"#;
let bytes = wav_with_ixml(xml.as_bytes());
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(
md.get("wav:ixml.body_len"),
Some(&xml.len().to_string()),
"raw chunk-body length must surface verbatim"
);
assert_eq!(
md.get("wav:ixml").map(|s| s.as_str()),
Some(xml.trim()),
"iXML text payload must round-trip"
);
assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
}
#[test]
fn ixml_trailing_nuls_trimmed_in_text_but_body_len_kept() {
let mut body = b"<BWFXML><PROJECT>OAV</PROJECT></BWFXML>".to_vec();
body.resize(body.len() + 64, 0);
let bytes = wav_with_ixml(&body);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(
md.get("wav:ixml"),
Some(&"<BWFXML><PROJECT>OAV</PROJECT></BWFXML>".to_string())
);
assert_eq!(md.get("wav:ixml.body_len"), Some(&body.len().to_string()));
}
#[test]
fn ixml_empty_body_surfaces_only_body_len() {
let bytes = wav_with_ixml(&[]);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:ixml.body_len"), Some(&"0".to_string()));
assert!(!md.contains_key("wav:ixml"));
assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
}
#[test]
fn ixml_whitespace_only_body_omits_text_key() {
let body = b" \t\r\n \0\0\0".to_vec();
let bytes = wav_with_ixml(&body);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:ixml.body_len"), Some(&body.len().to_string()));
assert!(!md.contains_key("wav:ixml"));
}
#[test]
fn ixml_odd_body_padding() {
let body = b"<X>1</X><Y>2</Y>!".to_vec();
assert_eq!(body.len() % 2, 1);
let bytes = wav_with_ixml(&body);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:ixml"), Some(&"<X>1</X><Y>2</Y>!".to_string()));
assert_eq!(md.get("wav:ixml.body_len"), Some(&"17".to_string()));
assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
}
fn wav_with_axml(axml_body: &[u8]) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(b"RIFF");
buf.extend_from_slice(&0u32.to_le_bytes());
buf.extend_from_slice(b"WAVE");
buf.extend_from_slice(b"fmt ");
buf.extend_from_slice(&16u32.to_le_bytes());
buf.extend_from_slice(&FMT_PCM.to_le_bytes());
buf.extend_from_slice(&1u16.to_le_bytes());
buf.extend_from_slice(&8_000u32.to_le_bytes());
buf.extend_from_slice(&16_000u32.to_le_bytes());
buf.extend_from_slice(&2u16.to_le_bytes());
buf.extend_from_slice(&16u16.to_le_bytes());
buf.extend_from_slice(b"axml");
buf.extend_from_slice(&(axml_body.len() as u32).to_le_bytes());
buf.extend_from_slice(axml_body);
if axml_body.len() % 2 == 1 {
buf.push(0);
}
buf.extend_from_slice(b"data");
buf.extend_from_slice(&0u32.to_le_bytes());
buf
}
#[test]
fn axml_canonical_ebucore_adm_document_round_trips() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<ebuCoreMain xmlns="urn:ebu:metadata-schema:ebucore"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<coreMetadata>
<format>
<audioFormatExtended>
<audioProgramme audioProgrammeID="APR_1001"
audioProgrammeName="OxideAV Round 258 demo">
<audioContentIDRef>ACO_1001</audioContentIDRef>
</audioProgramme>
</audioFormatExtended>
</format>
</coreMetadata>
</ebuCoreMain>"#;
let bytes = wav_with_axml(xml.as_bytes());
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(
md.get("wav:axml.body_len"),
Some(&xml.len().to_string()),
"raw chunk-body length must surface verbatim"
);
assert_eq!(
md.get("wav:axml").map(|s| s.as_str()),
Some(xml.trim()),
"axml text payload must round-trip"
);
assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
}
#[test]
fn axml_isrc_identifier_document_round_trips() {
let xml = r#"<ebuCoreMain xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns="urn:ebu:metadata-schema:ebucore">
<coreMetadata>
<identifier typeLabel="GUID" typeDefinition="Globally Unique Identifier"
formatLabel="ISRC" formatDefinition="International Standard Recording Code">
<dc:identifier>ISRC:NOX001212345</dc:identifier>
</identifier>
</coreMetadata>
</ebuCoreMain>"#;
let bytes = wav_with_axml(xml.as_bytes());
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:axml").map(|s| s.as_str()), Some(xml.trim()));
assert!(
md.get("wav:axml")
.map(|s| s.contains("ISRC:NOX001212345"))
.unwrap_or(false),
"ISRC identifier must survive the round-trip"
);
}
#[test]
fn axml_trailing_nuls_trimmed_in_text_but_body_len_kept() {
let mut body = b"<ebuCoreMain><id>X</id></ebuCoreMain>".to_vec();
body.resize(body.len() + 128, 0);
let bytes = wav_with_axml(&body);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(
md.get("wav:axml"),
Some(&"<ebuCoreMain><id>X</id></ebuCoreMain>".to_string())
);
assert_eq!(md.get("wav:axml.body_len"), Some(&body.len().to_string()));
}
#[test]
fn axml_empty_body_surfaces_only_body_len() {
let bytes = wav_with_axml(&[]);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:axml.body_len"), Some(&"0".to_string()));
assert!(!md.contains_key("wav:axml"));
assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
}
#[test]
fn axml_whitespace_only_body_omits_text_key() {
let body = b" \t\r\n \0\0\0".to_vec();
let bytes = wav_with_axml(&body);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:axml.body_len"), Some(&body.len().to_string()));
assert!(!md.contains_key("wav:axml"));
}
#[test]
fn axml_odd_body_padding() {
let body = b"<root><id>z</id></root>!!".to_vec();
assert_eq!(body.len() % 2, 1);
let bytes = wav_with_axml(&body);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(
md.get("wav:axml"),
Some(&"<root><id>z</id></root>!!".to_string())
);
assert_eq!(md.get("wav:axml.body_len"), Some(&"25".to_string()));
assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
}
fn wav_with_pmx(pmx_body: &[u8]) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(b"RIFF");
buf.extend_from_slice(&0u32.to_le_bytes());
buf.extend_from_slice(b"WAVE");
buf.extend_from_slice(b"fmt ");
buf.extend_from_slice(&16u32.to_le_bytes());
buf.extend_from_slice(&FMT_PCM.to_le_bytes());
buf.extend_from_slice(&1u16.to_le_bytes());
buf.extend_from_slice(&8_000u32.to_le_bytes());
buf.extend_from_slice(&16_000u32.to_le_bytes());
buf.extend_from_slice(&2u16.to_le_bytes());
buf.extend_from_slice(&16u16.to_le_bytes());
buf.extend_from_slice(b"_PMX");
buf.extend_from_slice(&(pmx_body.len() as u32).to_le_bytes());
buf.extend_from_slice(pmx_body);
if pmx_body.len() % 2 == 1 {
buf.push(0);
}
buf.extend_from_slice(b"data");
buf.extend_from_slice(&0u32.to_le_bytes());
buf
}
#[test]
fn pmx_canonical_xmp_packet_round_trips() {
let xml = r#"<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/">
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about=""
xmlns:dc="http://purl.org/dc/elements/1.1/">
<dc:title>
<rdf:Alt>
<rdf:li xml:lang="x-default">OxideAV Round 263 Fixture</rdf:li>
</rdf:Alt>
</dc:title>
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>
<?xpacket end="w"?>"#;
let bytes = wav_with_pmx(xml.as_bytes());
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(
md.get("wav:xmp.body_len"),
Some(&xml.len().to_string()),
"raw chunk-body length must surface verbatim"
);
assert_eq!(
md.get("wav:xmp").map(|s| s.as_str()),
Some(xml.trim()),
"XMP packet text must round-trip"
);
assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
}
#[test]
fn pmx_trailing_nuls_trimmed_in_text_but_body_len_kept() {
let mut body = b"<x:xmpmeta xmlns:x=\"adobe:ns:meta/\"/>".to_vec();
body.resize(body.len() + 96, 0);
let bytes = wav_with_pmx(&body);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(
md.get("wav:xmp"),
Some(&"<x:xmpmeta xmlns:x=\"adobe:ns:meta/\"/>".to_string())
);
assert_eq!(md.get("wav:xmp.body_len"), Some(&body.len().to_string()));
}
#[test]
fn pmx_empty_body_surfaces_only_body_len() {
let bytes = wav_with_pmx(&[]);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:xmp.body_len"), Some(&"0".to_string()));
assert!(!md.contains_key("wav:xmp"));
assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
}
#[test]
fn pmx_whitespace_only_body_omits_text_key() {
let body = b" \t\r\n \0\0\0".to_vec();
let bytes = wav_with_pmx(&body);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:xmp.body_len"), Some(&body.len().to_string()));
assert!(!md.contains_key("wav:xmp"));
}
#[test]
fn pmx_odd_body_padding() {
let body = b"<x:xmpmeta xmlns:x=\"a:n\"/>!".to_vec();
assert_eq!(body.len() % 2, 1);
let bytes = wav_with_pmx(&body);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(
md.get("wav:xmp"),
Some(&"<x:xmpmeta xmlns:x=\"a:n\"/>!".to_string())
);
assert_eq!(md.get("wav:xmp.body_len"), Some(&"27".to_string()));
assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
}
#[test]
fn pmx_absent_chunk_emits_no_xmp_keys() {
let mut buf = Vec::new();
buf.extend_from_slice(b"RIFF");
buf.extend_from_slice(&0u32.to_le_bytes());
buf.extend_from_slice(b"WAVE");
buf.extend_from_slice(b"fmt ");
buf.extend_from_slice(&16u32.to_le_bytes());
buf.extend_from_slice(&FMT_PCM.to_le_bytes());
buf.extend_from_slice(&1u16.to_le_bytes());
buf.extend_from_slice(&8_000u32.to_le_bytes());
buf.extend_from_slice(&16_000u32.to_le_bytes());
buf.extend_from_slice(&2u16.to_le_bytes());
buf.extend_from_slice(&16u16.to_le_bytes());
buf.extend_from_slice(b"data");
buf.extend_from_slice(&0u32.to_le_bytes());
let dmx = open_demux_from_bytes(buf);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert!(!md.contains_key("wav:xmp"));
assert!(!md.contains_key("wav:xmp.body_len"));
}
fn wav_with_cset(cset_body: &[u8]) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(b"RIFF");
buf.extend_from_slice(&0u32.to_le_bytes());
buf.extend_from_slice(b"WAVE");
buf.extend_from_slice(b"fmt ");
buf.extend_from_slice(&16u32.to_le_bytes());
buf.extend_from_slice(&FMT_PCM.to_le_bytes());
buf.extend_from_slice(&1u16.to_le_bytes());
buf.extend_from_slice(&8_000u32.to_le_bytes());
buf.extend_from_slice(&16_000u32.to_le_bytes());
buf.extend_from_slice(&2u16.to_le_bytes());
buf.extend_from_slice(&16u16.to_le_bytes());
buf.extend_from_slice(b"CSET");
buf.extend_from_slice(&(cset_body.len() as u32).to_le_bytes());
buf.extend_from_slice(cset_body);
if cset_body.len() % 2 == 1 {
buf.push(0);
}
buf.extend_from_slice(b"data");
buf.extend_from_slice(&0u32.to_le_bytes());
buf
}
fn cset_body(code_page: u16, country: u16, language: u16, dialect: u16) -> Vec<u8> {
let mut body = Vec::with_capacity(8);
body.extend_from_slice(&code_page.to_le_bytes());
body.extend_from_slice(&country.to_le_bytes());
body.extend_from_slice(&language.to_le_bytes());
body.extend_from_slice(&dialect.to_le_bytes());
body
}
#[test]
fn cset_canonical_uk_english_round_trips() {
let body = cset_body(1252, 44, 9, 2);
let bytes = wav_with_cset(&body);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:cset.body_len"), Some(&"8".to_string()));
assert_eq!(md.get("wav:cset.code_page"), Some(&"1252".to_string()));
assert_eq!(md.get("wav:cset.country"), Some(&"44".to_string()));
assert_eq!(
md.get("wav:cset.country_name"),
Some(&"United Kingdom".to_string())
);
assert_eq!(md.get("wav:cset.language"), Some(&"9".to_string()));
assert_eq!(md.get("wav:cset.dialect"), Some(&"2".to_string()));
assert_eq!(
md.get("wav:cset.language_name"),
Some(&"UK English".to_string())
);
assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
}
#[test]
fn cset_all_zero_uses_spec_defaults() {
let body = cset_body(0, 0, 0, 0);
let bytes = wav_with_cset(&body);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:cset.code_page"), Some(&"0".to_string()));
assert_eq!(md.get("wav:cset.country"), Some(&"0".to_string()));
assert_eq!(
md.get("wav:cset.country_name"),
Some(&"None".to_string()),
"wCountryCode = 0 must resolve to the ยง3 'None' placeholder"
);
assert_eq!(md.get("wav:cset.language"), Some(&"0".to_string()));
assert_eq!(md.get("wav:cset.dialect"), Some(&"0".to_string()));
assert_eq!(
md.get("wav:cset.language_name"),
Some(&"None".to_string()),
"wLanguageCode = 0 must resolve to the ยง3 'None' placeholder regardless of dialect"
);
}
#[test]
fn cset_unknown_codes_emit_raw_values_only() {
let body = cset_body(65001, 999, 99, 99);
let bytes = wav_with_cset(&body);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:cset.code_page"), Some(&"65001".to_string()));
assert_eq!(md.get("wav:cset.country"), Some(&"999".to_string()));
assert!(
!md.contains_key("wav:cset.country_name"),
"unknown country must not synthesise a human-readable name"
);
assert_eq!(md.get("wav:cset.language"), Some(&"99".to_string()));
assert_eq!(md.get("wav:cset.dialect"), Some(&"99".to_string()));
assert!(
!md.contains_key("wav:cset.language_name"),
"unknown language pair must not synthesise a human-readable name"
);
}
#[test]
fn cset_short_body_treated_as_opaque() {
let body = vec![0x52, 0x04, 0x00, 0x00];
let bytes = wav_with_cset(&body);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:cset.body_len"), Some(&"4".to_string()));
assert!(!md.contains_key("wav:cset.code_page"));
assert!(!md.contains_key("wav:cset.country"));
assert!(!md.contains_key("wav:cset.language"));
assert!(!md.contains_key("wav:cset.dialect"));
assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
}
#[test]
fn cset_oversized_body_tolerates_trailing_bytes() {
let mut body = cset_body(932, 81, 17, 1); body.extend_from_slice(&[0xFE, 0xCA, 0xAD, 0xDE]);
assert_eq!(body.len(), 12);
let bytes = wav_with_cset(&body);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:cset.body_len"), Some(&"12".to_string()));
assert_eq!(md.get("wav:cset.code_page"), Some(&"932".to_string()));
assert_eq!(md.get("wav:cset.country"), Some(&"81".to_string()));
assert_eq!(md.get("wav:cset.country_name"), Some(&"Japan".to_string()));
assert_eq!(md.get("wav:cset.language"), Some(&"17".to_string()));
assert_eq!(md.get("wav:cset.dialect"), Some(&"1".to_string()));
assert_eq!(
md.get("wav:cset.language_name"),
Some(&"Japanese".to_string())
);
}
#[test]
fn cset_coexists_with_list_info() {
let mut buf = Vec::new();
buf.extend_from_slice(b"RIFF");
buf.extend_from_slice(&0u32.to_le_bytes());
buf.extend_from_slice(b"WAVE");
buf.extend_from_slice(b"fmt ");
buf.extend_from_slice(&16u32.to_le_bytes());
buf.extend_from_slice(&FMT_PCM.to_le_bytes());
buf.extend_from_slice(&1u16.to_le_bytes());
buf.extend_from_slice(&8_000u32.to_le_bytes());
buf.extend_from_slice(&16_000u32.to_le_bytes());
buf.extend_from_slice(&2u16.to_le_bytes());
buf.extend_from_slice(&16u16.to_le_bytes());
let cset = cset_body(1252, 33, 12, 1);
buf.extend_from_slice(b"CSET");
buf.extend_from_slice(&(cset.len() as u32).to_le_bytes());
buf.extend_from_slice(&cset);
let mut list_body = Vec::new();
list_body.extend_from_slice(b"INFO");
list_body.extend_from_slice(b"INAM");
list_body.extend_from_slice(&1u32.to_le_bytes());
list_body.extend_from_slice(b"T");
list_body.push(0); buf.extend_from_slice(b"LIST");
buf.extend_from_slice(&(list_body.len() as u32).to_le_bytes());
buf.extend_from_slice(&list_body);
buf.extend_from_slice(b"data");
buf.extend_from_slice(&0u32.to_le_bytes());
let dmx = open_demux_from_bytes(buf);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:cset.code_page"), Some(&"1252".to_string()));
assert_eq!(md.get("wav:cset.country_name"), Some(&"France".to_string()));
assert_eq!(
md.get("wav:cset.language_name"),
Some(&"French".to_string())
);
assert_eq!(md.get("title"), Some(&"T".to_string()));
}
#[test]
fn cset_odd_body_padding() {
let mut body = cset_body(1252, 1, 9, 1);
body.push(0xAA);
assert_eq!(body.len() % 2, 1);
let bytes = wav_with_cset(&body);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:cset.body_len"), Some(&"9".to_string()));
assert_eq!(md.get("wav:cset.code_page"), Some(&"1252".to_string()));
assert_eq!(md.get("wav:cset.country_name"), Some(&"USA".to_string()));
assert_eq!(
md.get("wav:cset.language_name"),
Some(&"US English".to_string())
);
assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
}
#[test]
fn cset_country_name_table_spot_checks() {
assert_eq!(cset_country_name(1), Some("USA"));
assert_eq!(cset_country_name(44), Some("United Kingdom"));
assert_eq!(cset_country_name(351), Some("Portugal"));
assert_eq!(cset_country_name(358), Some("Finland"));
assert_eq!(cset_country_name(0), Some("None"));
assert_eq!(cset_country_name(500), None);
}
#[test]
fn cset_language_name_table_spot_checks() {
assert_eq!(cset_language_name(9, 1), Some("US English"));
assert_eq!(cset_language_name(9, 2), Some("UK English"));
assert_eq!(cset_language_name(12, 2), Some("Belgian French"));
assert_eq!(cset_language_name(12, 3), Some("Canadian French"));
assert_eq!(cset_language_name(12, 4), Some("Swiss French"));
assert_eq!(cset_language_name(26, 1), Some("Serbo-Croatian (Latin)"));
assert_eq!(cset_language_name(26, 2), Some("Serbo-Croatian (Cyrillic)"));
assert_eq!(cset_language_name(0, 0), Some("None"));
assert_eq!(cset_language_name(0, 1), Some("None"));
assert_eq!(cset_language_name(9, 9), None);
}
fn wav_with_junk(junk_sizes: &[usize]) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(b"RIFF");
buf.extend_from_slice(&0u32.to_le_bytes());
buf.extend_from_slice(b"WAVE");
buf.extend_from_slice(b"fmt ");
buf.extend_from_slice(&16u32.to_le_bytes());
buf.extend_from_slice(&FMT_PCM.to_le_bytes());
buf.extend_from_slice(&1u16.to_le_bytes());
buf.extend_from_slice(&8_000u32.to_le_bytes());
buf.extend_from_slice(&16_000u32.to_le_bytes());
buf.extend_from_slice(&2u16.to_le_bytes());
buf.extend_from_slice(&16u16.to_le_bytes());
for (i, &sz) in junk_sizes.iter().enumerate() {
buf.extend_from_slice(b"JUNK");
buf.extend_from_slice(&(sz as u32).to_le_bytes());
buf.extend(std::iter::repeat_n(i as u8, sz));
if sz % 2 == 1 {
buf.push(0);
}
}
buf.extend_from_slice(b"data");
buf.extend_from_slice(&0u32.to_le_bytes());
buf
}
#[test]
fn junk_single_chunk_surfaces_accounting_metadata() {
let bytes = wav_with_junk(&[16]);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:junk.count"), Some(&"1".to_string()));
assert_eq!(md.get("wav:junk.total_bytes"), Some(&"16".to_string()));
assert_eq!(md.get("wav:junk.0.body_len"), Some(&"16".to_string()));
assert!(
!md.keys()
.any(|k| k.starts_with("wav:junk") && k.ends_with(".body")),
"JUNK chunk body must not be surfaced (Microsoft RIFF MCI ยง2)"
);
assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
}
#[test]
fn junk_multiple_chunks_accumulate() {
let bytes = wav_with_junk(&[32, 8, 100]);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:junk.count"), Some(&"3".to_string()));
assert_eq!(
md.get("wav:junk.total_bytes"),
Some(&(32u64 + 8 + 100).to_string())
);
assert_eq!(md.get("wav:junk.0.body_len"), Some(&"32".to_string()));
assert_eq!(md.get("wav:junk.1.body_len"), Some(&"8".to_string()));
assert_eq!(md.get("wav:junk.2.body_len"), Some(&"100".to_string()));
assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
}
#[test]
fn junk_empty_body_still_counts() {
let bytes = wav_with_junk(&[0]);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:junk.count"), Some(&"1".to_string()));
assert_eq!(md.get("wav:junk.total_bytes"), Some(&"0".to_string()));
assert_eq!(md.get("wav:junk.0.body_len"), Some(&"0".to_string()));
}
#[test]
fn junk_absent_emits_no_keys() {
let bytes = wav_with_junk(&[]);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert!(
!md.keys().any(|k| k.starts_with("wav:junk")),
"no JUNK chunk โ no wav:junk.* metadata"
);
assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
}
#[test]
fn junk_odd_body_padding() {
let bytes = wav_with_junk(&[7]); let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:junk.0.body_len"), Some(&"7".to_string()));
assert_eq!(md.get("wav:junk.total_bytes"), Some(&"7".to_string()));
assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
}
#[test]
fn junk_coexists_with_other_chunks() {
let mut buf = Vec::new();
buf.extend_from_slice(b"RIFF");
buf.extend_from_slice(&0u32.to_le_bytes());
buf.extend_from_slice(b"WAVE");
buf.extend_from_slice(b"fmt ");
buf.extend_from_slice(&16u32.to_le_bytes());
buf.extend_from_slice(&FMT_PCM.to_le_bytes());
buf.extend_from_slice(&1u16.to_le_bytes());
buf.extend_from_slice(&8_000u32.to_le_bytes());
buf.extend_from_slice(&16_000u32.to_le_bytes());
buf.extend_from_slice(&2u16.to_le_bytes());
buf.extend_from_slice(&16u16.to_le_bytes());
buf.extend_from_slice(b"JUNK");
buf.extend_from_slice(&12u32.to_le_bytes());
buf.extend(std::iter::repeat_n(0xAAu8, 12));
let cset = cset_body(1252, 1, 9, 1);
buf.extend_from_slice(b"CSET");
buf.extend_from_slice(&(cset.len() as u32).to_le_bytes());
buf.extend_from_slice(&cset);
let mut list_body = Vec::new();
list_body.extend_from_slice(b"INFO");
list_body.extend_from_slice(b"INAM");
list_body.extend_from_slice(&1u32.to_le_bytes());
list_body.extend_from_slice(b"T");
list_body.push(0);
buf.extend_from_slice(b"LIST");
buf.extend_from_slice(&(list_body.len() as u32).to_le_bytes());
buf.extend_from_slice(&list_body);
buf.extend_from_slice(b"JUNK");
buf.extend_from_slice(&4u32.to_le_bytes());
buf.extend(std::iter::repeat_n(0xBBu8, 4));
buf.extend_from_slice(b"data");
buf.extend_from_slice(&0u32.to_le_bytes());
let dmx = open_demux_from_bytes(buf);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:junk.count"), Some(&"2".to_string()));
assert_eq!(md.get("wav:junk.total_bytes"), Some(&"16".to_string()));
assert_eq!(md.get("wav:junk.0.body_len"), Some(&"12".to_string()));
assert_eq!(md.get("wav:junk.1.body_len"), Some(&"4".to_string()));
assert_eq!(md.get("wav:cset.code_page"), Some(&"1252".to_string()));
assert_eq!(md.get("title"), Some(&"T".to_string()));
assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
}
fn wav_with_slnt(slnt_bodies: &[&[u8]]) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(b"RIFF");
buf.extend_from_slice(&0u32.to_le_bytes());
buf.extend_from_slice(b"WAVE");
buf.extend_from_slice(b"fmt ");
buf.extend_from_slice(&16u32.to_le_bytes());
buf.extend_from_slice(&FMT_PCM.to_le_bytes());
buf.extend_from_slice(&1u16.to_le_bytes());
buf.extend_from_slice(&8_000u32.to_le_bytes());
buf.extend_from_slice(&16_000u32.to_le_bytes());
buf.extend_from_slice(&2u16.to_le_bytes());
buf.extend_from_slice(&16u16.to_le_bytes());
for body in slnt_bodies {
buf.extend_from_slice(b"slnt");
buf.extend_from_slice(&(body.len() as u32).to_le_bytes());
buf.extend_from_slice(body);
if body.len() % 2 == 1 {
buf.push(0);
}
}
buf.extend_from_slice(b"data");
buf.extend_from_slice(&0u32.to_le_bytes());
buf
}
#[test]
fn slnt_single_chunk_surfaces_sample_count() {
let bytes = wav_with_slnt(&[&1_000u32.to_le_bytes()]);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:slnt.count"), Some(&"1".to_string()));
assert_eq!(md.get("wav:slnt.total_samples"), Some(&"1000".to_string()));
assert_eq!(md.get("wav:slnt.0.samples"), Some(&"1000".to_string()));
assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
}
#[test]
fn slnt_multiple_chunks_accumulate() {
let bytes = wav_with_slnt(&[
&500u32.to_le_bytes(),
&250u32.to_le_bytes(),
&44_100u32.to_le_bytes(),
]);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:slnt.count"), Some(&"3".to_string()));
assert_eq!(
md.get("wav:slnt.total_samples"),
Some(&(500u64 + 250 + 44_100).to_string())
);
assert_eq!(md.get("wav:slnt.0.samples"), Some(&"500".to_string()));
assert_eq!(md.get("wav:slnt.1.samples"), Some(&"250".to_string()));
assert_eq!(md.get("wav:slnt.2.samples"), Some(&"44100".to_string()));
assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
}
#[test]
fn slnt_zero_samples_still_counts() {
let bytes = wav_with_slnt(&[&0u32.to_le_bytes()]);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:slnt.count"), Some(&"1".to_string()));
assert_eq!(md.get("wav:slnt.total_samples"), Some(&"0".to_string()));
assert_eq!(md.get("wav:slnt.0.samples"), Some(&"0".to_string()));
}
#[test]
fn slnt_absent_emits_no_keys() {
let bytes = wav_with_slnt(&[]);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert!(
!md.keys().any(|k| k.starts_with("wav:slnt")),
"no slnt chunk โ no wav:slnt.* metadata"
);
assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
}
#[test]
fn slnt_short_body_is_opaque() {
let bytes = wav_with_slnt(&[&[0x01, 0x02, 0x03]]);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:slnt.count"), Some(&"1".to_string()));
assert_eq!(md.get("wav:slnt.total_samples"), Some(&"0".to_string()));
assert!(
!md.contains_key("wav:slnt.0.samples"),
"under-length slnt body must not surface a samples value"
);
assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
}
#[test]
fn slnt_long_body_decodes_leading_dword() {
let mut body = 7u32.to_le_bytes().to_vec();
body.push(0xFF);
let bytes = wav_with_slnt(&[&body]);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:slnt.count"), Some(&"1".to_string()));
assert_eq!(md.get("wav:slnt.0.samples"), Some(&"7".to_string()));
assert_eq!(md.get("wav:slnt.total_samples"), Some(&"7".to_string()));
assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
}
#[test]
fn slnt_coexists_with_other_chunks() {
let mut buf = Vec::new();
buf.extend_from_slice(b"RIFF");
buf.extend_from_slice(&0u32.to_le_bytes());
buf.extend_from_slice(b"WAVE");
buf.extend_from_slice(b"fmt ");
buf.extend_from_slice(&16u32.to_le_bytes());
buf.extend_from_slice(&FMT_PCM.to_le_bytes());
buf.extend_from_slice(&1u16.to_le_bytes());
buf.extend_from_slice(&8_000u32.to_le_bytes());
buf.extend_from_slice(&16_000u32.to_le_bytes());
buf.extend_from_slice(&2u16.to_le_bytes());
buf.extend_from_slice(&16u16.to_le_bytes());
buf.extend_from_slice(b"slnt");
buf.extend_from_slice(&4u32.to_le_bytes());
buf.extend_from_slice(&800u32.to_le_bytes());
buf.extend_from_slice(b"JUNK");
buf.extend_from_slice(&6u32.to_le_bytes());
buf.extend(std::iter::repeat_n(0xAAu8, 6));
let mut list_body = Vec::new();
list_body.extend_from_slice(b"INFO");
list_body.extend_from_slice(b"INAM");
list_body.extend_from_slice(&1u32.to_le_bytes());
list_body.extend_from_slice(b"T");
list_body.push(0);
buf.extend_from_slice(b"LIST");
buf.extend_from_slice(&(list_body.len() as u32).to_le_bytes());
buf.extend_from_slice(&list_body);
buf.extend_from_slice(b"slnt");
buf.extend_from_slice(&4u32.to_le_bytes());
buf.extend_from_slice(&200u32.to_le_bytes());
buf.extend_from_slice(b"data");
buf.extend_from_slice(&0u32.to_le_bytes());
let dmx = open_demux_from_bytes(buf);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:slnt.count"), Some(&"2".to_string()));
assert_eq!(md.get("wav:slnt.total_samples"), Some(&"1000".to_string()));
assert_eq!(md.get("wav:slnt.0.samples"), Some(&"800".to_string()));
assert_eq!(md.get("wav:slnt.1.samples"), Some(&"200".to_string()));
assert_eq!(md.get("wav:junk.count"), Some(&"1".to_string()));
assert_eq!(md.get("title"), Some(&"T".to_string()));
assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
}
enum WavlSeg<'a> {
Data(&'a [u8]),
Slnt(u32),
}
fn wav_with_wavl(segs: &[WavlSeg], fact_samples: u32) -> Vec<u8> {
let mut wavl = Vec::new();
wavl.extend_from_slice(b"wavl");
for seg in segs {
match seg {
WavlSeg::Data(body) => {
wavl.extend_from_slice(b"data");
wavl.extend_from_slice(&(body.len() as u32).to_le_bytes());
wavl.extend_from_slice(body);
if body.len() % 2 == 1 {
wavl.push(0);
}
}
WavlSeg::Slnt(samples) => {
wavl.extend_from_slice(b"slnt");
wavl.extend_from_slice(&4u32.to_le_bytes());
wavl.extend_from_slice(&samples.to_le_bytes());
}
}
}
let mut buf = Vec::new();
buf.extend_from_slice(b"RIFF");
buf.extend_from_slice(&0u32.to_le_bytes());
buf.extend_from_slice(b"WAVE");
buf.extend_from_slice(b"fmt ");
buf.extend_from_slice(&16u32.to_le_bytes());
buf.extend_from_slice(&FMT_PCM.to_le_bytes());
buf.extend_from_slice(&1u16.to_le_bytes());
buf.extend_from_slice(&8_000u32.to_le_bytes());
buf.extend_from_slice(&16_000u32.to_le_bytes());
buf.extend_from_slice(&2u16.to_le_bytes());
buf.extend_from_slice(&16u16.to_le_bytes());
buf.extend_from_slice(b"fact");
buf.extend_from_slice(&4u32.to_le_bytes());
buf.extend_from_slice(&fact_samples.to_le_bytes());
buf.extend_from_slice(b"LIST");
buf.extend_from_slice(&(wavl.len() as u32).to_le_bytes());
buf.extend_from_slice(&wavl);
buf
}
#[test]
fn wavl_single_data_segment_decodes() {
let pcm: Vec<u8> = (0..40u8).collect();
let bytes = wav_with_wavl(&[WavlSeg::Data(&pcm)], 20);
let mut dmx = open_demux_from_bytes(bytes);
assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
let mut out = Vec::new();
loop {
match dmx.next_packet() {
Ok(p) => out.extend_from_slice(&p.data),
Err(Error::Eof) => break,
Err(e) => panic!("demux error: {e}"),
}
}
assert_eq!(out, pcm);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:wavl.segment_count"), Some(&"1".to_string()));
assert_eq!(md.get("wav:wavl.data_count"), Some(&"1".to_string()));
assert_eq!(md.get("wav:wavl.data_bytes"), Some(&"40".to_string()));
assert_eq!(md.get("wav:wavl.0.kind"), Some(&"data".to_string()));
assert_eq!(md.get("wav:wavl.0.length"), Some(&"40".to_string()));
}
#[test]
fn wavl_interleaved_data_silence_surfaces_segments() {
let a: Vec<u8> = (0..8u8).collect();
let b: Vec<u8> = (100..108u8).collect();
let bytes = wav_with_wavl(
&[WavlSeg::Data(&a), WavlSeg::Slnt(500), WavlSeg::Data(&b)],
1000,
);
let mut dmx = open_demux_from_bytes(bytes);
let mut out = Vec::new();
loop {
match dmx.next_packet() {
Ok(p) => out.extend_from_slice(&p.data),
Err(Error::Eof) => break,
Err(e) => panic!("demux error: {e}"),
}
}
assert_eq!(out, a);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:wavl.segment_count"), Some(&"3".to_string()));
assert_eq!(md.get("wav:wavl.data_count"), Some(&"2".to_string()));
assert_eq!(md.get("wav:wavl.data_bytes"), Some(&"16".to_string()));
assert_eq!(md.get("wav:wavl.0.kind"), Some(&"data".to_string()));
assert_eq!(md.get("wav:wavl.1.kind"), Some(&"slnt".to_string()));
assert_eq!(md.get("wav:wavl.2.kind"), Some(&"data".to_string()));
assert_eq!(md.get("wav:slnt.count"), Some(&"1".to_string()));
assert_eq!(md.get("wav:slnt.total_samples"), Some(&"500".to_string()));
assert_eq!(md.get("wav:slnt.0.samples"), Some(&"500".to_string()));
let s = &dmx.streams()[0];
assert_eq!(s.duration, Some(1000));
}
#[test]
fn wavl_silence_only_has_no_waveform() {
let bytes = wav_with_wavl(&[WavlSeg::Slnt(1000), WavlSeg::Slnt(2000)], 3000);
use std::io::Cursor;
let rs: Box<dyn ReadSeek> = Box::new(Cursor::new(bytes));
match open_demuxer(rs, &oxideav_core::NullCodecResolver) {
Ok(_) => panic!("silence-only wavl must be rejected as having no waveform"),
Err(Error::InvalidData(_)) => {}
Err(e) => panic!("expected InvalidData, got {e:?}"),
}
}
#[test]
fn wavl_odd_data_segment_padding() {
let a: Vec<u8> = (0..7u8).collect(); let b: Vec<u8> = (50..54u8).collect();
let bytes = wav_with_wavl(&[WavlSeg::Data(&a), WavlSeg::Data(&b)], 100);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:wavl.segment_count"), Some(&"2".to_string()));
assert_eq!(md.get("wav:wavl.0.length"), Some(&"7".to_string()));
assert_eq!(md.get("wav:wavl.1.kind"), Some(&"data".to_string()));
assert_eq!(md.get("wav:wavl.1.length"), Some(&"4".to_string()));
}
fn find_chunk(buf: &[u8], fourcc: &[u8; 4]) -> Option<usize> {
let mut i = 12usize;
while i + 8 <= buf.len() {
if &buf[i..i + 4] == fourcc {
return Some(i);
}
let sz = u32::from_le_bytes([buf[i + 4], buf[i + 5], buf[i + 6], buf[i + 7]]) as usize;
i += 8 + sz + (sz % 2);
}
None
}
#[test]
fn guid_canonical_text() {
assert_eq!(fmt_guid(&GUID_PCM), "00000001-0000-0010-8000-00AA00389B71");
assert_eq!(
fmt_guid(&GUID_IEEE_FLOAT),
"00000003-0000-0010-8000-00AA00389B71"
);
assert_eq!(fmt_guid(&GUID_ALAW), "00000006-0000-0010-8000-00AA00389B71");
assert_eq!(
fmt_guid(&GUID_MULAW),
"00000007-0000-0010-8000-00AA00389B71"
);
}
fn info_subchunk(id: &[u8; 4], text: &str) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(id);
let payload_len = text.len() + 1;
out.extend_from_slice(&(payload_len as u32).to_le_bytes());
out.extend_from_slice(text.as_bytes());
out.push(0);
if payload_len % 2 == 1 {
out.push(0);
}
out
}
fn wav_with_info_entries(entries: &[(&[u8; 4], &str)]) -> Vec<u8> {
let mut list_body = Vec::new();
list_body.extend_from_slice(b"INFO");
for (id, text) in entries {
list_body.extend_from_slice(&info_subchunk(id, text));
}
let mut buf = Vec::new();
buf.extend_from_slice(b"RIFF");
buf.extend_from_slice(&0u32.to_le_bytes());
buf.extend_from_slice(b"WAVE");
buf.extend_from_slice(b"fmt ");
buf.extend_from_slice(&16u32.to_le_bytes());
buf.extend_from_slice(&FMT_PCM.to_le_bytes());
buf.extend_from_slice(&1u16.to_le_bytes());
buf.extend_from_slice(&8_000u32.to_le_bytes());
buf.extend_from_slice(&16_000u32.to_le_bytes());
buf.extend_from_slice(&2u16.to_le_bytes());
buf.extend_from_slice(&16u16.to_le_bytes());
buf.extend_from_slice(b"LIST");
buf.extend_from_slice(&(list_body.len() as u32).to_le_bytes());
buf.extend_from_slice(&list_body);
buf.extend_from_slice(b"data");
buf.extend_from_slice(&0u32.to_le_bytes());
buf
}
#[test]
fn info_list_full_baseline_round_trip() {
let entries: &[(&[u8; 4], &str)] = &[
(b"IARL", "Library of Congress"),
(b"IART", "Michaelangelo"),
(b"ICMS", "Pope Julian II"),
(b"ICMT", "General comment."),
(b"ICOP", "Copyright Encyclopedia International 1991"),
(b"ICRD", "1553-05-03"),
(b"ICRP", "lower right corner"),
(b"IDIM", "8.5 in h, 11 in w"),
(b"IDPI", "300"),
(b"IENG", "Smith, John; Adams, Joe"),
(b"IGNR", "landscape"),
(b"IKEY", "Seattle; aerial view; scenery"),
(b"ILGT", "+10"),
(b"IMED", "lithograph"),
(b"INAM", "Seattle From Above"),
(b"IPLT", "256"),
(b"IPRD", "Encyclopedia of Pacific Northwest Geography"),
(b"ISBJ", "Aerial view of Seattle"),
(b"ISFT", "Microsoft WaveEdit"),
(b"ISHP", "+5"),
(b"ISRC", "Trey Research"),
(b"ISRF", "slide"),
(b"ITCH", "Smith, John"),
];
let bytes = wav_with_info_entries(entries);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
let expected: &[(&str, &str)] = &[
("archival_location", "Library of Congress"),
("artist", "Michaelangelo"),
("commissioned", "Pope Julian II"),
("comment", "General comment."),
("copyright", "Copyright Encyclopedia International 1991"),
("date", "1553-05-03"),
("cropped", "lower right corner"),
("dimensions", "8.5 in h, 11 in w"),
("dpi", "300"),
("engineer", "Smith, John; Adams, Joe"),
("genre", "landscape"),
("keywords", "Seattle; aerial view; scenery"),
("lightness", "+10"),
("medium", "lithograph"),
("title", "Seattle From Above"),
("palette_setting", "256"),
("album", "Encyclopedia of Pacific Northwest Geography"),
("subject", "Aerial view of Seattle"),
("encoder", "Microsoft WaveEdit"),
("sharpness", "+5"),
("source", "Trey Research"),
("source_form", "slide"),
("technician", "Smith, John"),
];
for (key, value) in expected {
assert_eq!(
md.get(*key),
Some(&(*value).to_string()),
"INFO key {key:?} missing or wrong; got {:?}",
md.get(*key)
);
}
}
#[test]
fn info_id_to_key_legacy_aliases_preserved() {
assert_eq!(info_id_to_key(b"INAM"), Some("title"));
assert_eq!(info_id_to_key(b"IART"), Some("artist"));
assert_eq!(info_id_to_key(b"IPRD"), Some("album"));
assert_eq!(info_id_to_key(b"ICMT"), Some("comment"));
assert_eq!(info_id_to_key(b"ICRD"), Some("date"));
assert_eq!(info_id_to_key(b"IGNR"), Some("genre"));
assert_eq!(info_id_to_key(b"ICOP"), Some("copyright"));
assert_eq!(info_id_to_key(b"IENG"), Some("engineer"));
assert_eq!(info_id_to_key(b"ITCH"), Some("technician"));
assert_eq!(info_id_to_key(b"ISFT"), Some("encoder"));
assert_eq!(info_id_to_key(b"ISBJ"), Some("subject"));
assert_eq!(info_id_to_key(b"ITRK"), Some("track"));
}
#[test]
fn info_id_to_key_baseline_completeness() {
const BASELINE: &[&[u8; 4]] = &[
b"IARL", b"IART", b"ICMS", b"ICMT", b"ICOP", b"ICRD", b"ICRP", b"IDIM", b"IDPI",
b"IENG", b"IGNR", b"IKEY", b"ILGT", b"IMED", b"INAM", b"IPLT", b"IPRD", b"ISBJ",
b"ISFT", b"ISHP", b"ISRC", b"ISRF", b"ITCH",
];
for id in BASELINE {
assert!(
info_id_to_key(id).is_some(),
"baseline INFO sub-ID {:?} missing from info_id_to_key",
std::str::from_utf8(*id).unwrap()
);
}
assert_eq!(info_id_to_key(b"ZZZZ"), None);
assert_eq!(info_id_to_key(b"IRAL"), None);
}
#[test]
fn info_list_unknown_subchunk_is_skipped() {
let entries: &[(&[u8; 4], &str)] = &[
(b"INAM", "Title"),
(b"IZZZ", "ignored"),
(b"IART", "Artist"),
];
let bytes = wav_with_info_entries(entries);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("title"), Some(&"Title".to_string()));
assert_eq!(md.get("artist"), Some(&"Artist".to_string()));
assert!(md
.keys()
.all(|k| !k.contains("IZZZ") && !k.contains("izzz")));
}
#[allow(clippy::too_many_arguments)]
fn synth_rf64(
magic: &[u8; 4],
ds64_data_size: u64,
ds64_sample_count: u64,
extra_table: &[([u8; 4], u64)],
data_payload: &[u8],
channels: u16,
sample_rate: u32,
bits_per_sample: u16,
) -> Vec<u8> {
let mut ds64_body: Vec<u8> = Vec::new();
ds64_body.extend_from_slice(&0u64.to_le_bytes());
ds64_body.extend_from_slice(&ds64_data_size.to_le_bytes());
ds64_body.extend_from_slice(&ds64_sample_count.to_le_bytes());
ds64_body.extend_from_slice(&(extra_table.len() as u32).to_le_bytes());
for (id, sz) in extra_table {
ds64_body.extend_from_slice(id);
ds64_body.extend_from_slice(&sz.to_le_bytes());
}
let block_align = (bits_per_sample / 8) * channels;
let byte_rate = sample_rate * block_align as u32;
let mut fmt_body: Vec<u8> = Vec::new();
fmt_body.extend_from_slice(&WAVE_FORMAT_PCM.to_le_bytes());
fmt_body.extend_from_slice(&channels.to_le_bytes());
fmt_body.extend_from_slice(&sample_rate.to_le_bytes());
fmt_body.extend_from_slice(&byte_rate.to_le_bytes());
fmt_body.extend_from_slice(&block_align.to_le_bytes());
fmt_body.extend_from_slice(&bits_per_sample.to_le_bytes());
let fact_body = SIZE64_SENTINEL.to_le_bytes();
let mut out: Vec<u8> = Vec::new();
out.extend_from_slice(magic);
out.extend_from_slice(&SIZE64_SENTINEL.to_le_bytes());
out.extend_from_slice(b"WAVE");
out.extend_from_slice(b"ds64");
out.extend_from_slice(&(ds64_body.len() as u32).to_le_bytes());
let ds64_riff_field_off = out.len();
out.extend_from_slice(&ds64_body);
out.extend_from_slice(b"fmt ");
out.extend_from_slice(&(fmt_body.len() as u32).to_le_bytes());
out.extend_from_slice(&fmt_body);
out.extend_from_slice(b"fact");
out.extend_from_slice(&(fact_body.len() as u32).to_le_bytes());
out.extend_from_slice(&fact_body);
out.extend_from_slice(b"data");
out.extend_from_slice(&SIZE64_SENTINEL.to_le_bytes());
out.extend_from_slice(data_payload);
let riff_size_64 = (out.len() as u64) - 8;
out[ds64_riff_field_off..ds64_riff_field_off + 8]
.copy_from_slice(&riff_size_64.to_le_bytes());
out
}
#[test]
fn rf64_demux_pcm_s16_with_ds64_overrides() {
let payload: Vec<u8> = (0..200u8).collect();
let bytes = synth_rf64(b"RF64", 200, 100, &[], &payload, 1, 48_000, 16);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:rf64.magic"), Some(&"RF64".to_string()));
assert_eq!(md.get("wav:rf64.data_size"), Some(&"200".to_string()));
assert_eq!(md.get("wav:rf64.sample_count"), Some(&"100".to_string()));
assert_eq!(md.get("wav:rf64.table.count"), Some(&"0".to_string()));
let stream = &dmx.streams()[0];
assert_eq!(stream.duration, Some(100));
assert_eq!(stream.params.codec_id, CodecId::new("pcm_s16le"));
}
#[test]
fn bw64_demux_pcm_s16_treated_as_rf64() {
let payload: Vec<u8> = (0..40u8).collect();
let bytes = synth_rf64(b"BW64", 40, 20, &[], &payload, 1, 48_000, 16);
let dmx = open_demux_from_bytes(bytes);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:rf64.magic"), Some(&"BW64".to_string()));
assert_eq!(md.get("wav:rf64.data_size"), Some(&"40".to_string()));
assert_eq!(md.get("wav:rf64.sample_count"), Some(&"20".to_string()));
assert_eq!(dmx.streams()[0].duration, Some(20));
}
#[test]
fn rf64_table_promotes_non_data_chunk_size() {
let mut list_body: Vec<u8> = Vec::new();
list_body.extend_from_slice(b"INFO");
list_body.extend_from_slice(b"INAM");
list_body.extend_from_slice(&6u32.to_le_bytes()); list_body.extend_from_slice(b"Hello\0");
let list_body_len = list_body.len() as u64;
let mut ds64_body: Vec<u8> = Vec::new();
ds64_body.extend_from_slice(&0u64.to_le_bytes()); ds64_body.extend_from_slice(&8u64.to_le_bytes()); ds64_body.extend_from_slice(&4u64.to_le_bytes()); ds64_body.extend_from_slice(&1u32.to_le_bytes()); ds64_body.extend_from_slice(b"LIST");
ds64_body.extend_from_slice(&list_body_len.to_le_bytes());
let payload: Vec<u8> = (0..8u8).collect();
let fmt_body = {
let mut f = Vec::new();
f.extend_from_slice(&WAVE_FORMAT_PCM.to_le_bytes());
f.extend_from_slice(&1u16.to_le_bytes()); f.extend_from_slice(&48_000u32.to_le_bytes());
f.extend_from_slice(&(48_000u32 * 2).to_le_bytes());
f.extend_from_slice(&2u16.to_le_bytes()); f.extend_from_slice(&16u16.to_le_bytes());
f
};
let mut out: Vec<u8> = Vec::new();
out.extend_from_slice(b"RF64");
out.extend_from_slice(&SIZE64_SENTINEL.to_le_bytes());
out.extend_from_slice(b"WAVE");
out.extend_from_slice(b"ds64");
out.extend_from_slice(&(ds64_body.len() as u32).to_le_bytes());
let ds64_riff_off = out.len();
out.extend_from_slice(&ds64_body);
out.extend_from_slice(b"fmt ");
out.extend_from_slice(&(fmt_body.len() as u32).to_le_bytes());
out.extend_from_slice(&fmt_body);
out.extend_from_slice(b"LIST");
out.extend_from_slice(&SIZE64_SENTINEL.to_le_bytes());
out.extend_from_slice(&list_body);
out.extend_from_slice(b"data");
out.extend_from_slice(&SIZE64_SENTINEL.to_le_bytes());
out.extend_from_slice(&payload);
let riff_size_64 = (out.len() as u64) - 8;
out[ds64_riff_off..ds64_riff_off + 8].copy_from_slice(&riff_size_64.to_le_bytes());
let dmx = open_demux_from_bytes(out);
let md: std::collections::HashMap<String, String> =
dmx.metadata().iter().cloned().collect();
assert_eq!(md.get("wav:rf64.table.count"), Some(&"1".to_string()));
assert_eq!(md.get("wav:rf64.table.0.id"), Some(&"LIST".to_string()));
assert_eq!(
md.get("wav:rf64.table.0.size"),
Some(&list_body_len.to_string())
);
assert_eq!(md.get("title"), Some(&"Hello".to_string()));
}
#[test]
fn rf64_sentinel_without_ds64_is_rejected() {
let fmt_body = {
let mut f = Vec::new();
f.extend_from_slice(&WAVE_FORMAT_PCM.to_le_bytes());
f.extend_from_slice(&1u16.to_le_bytes());
f.extend_from_slice(&48_000u32.to_le_bytes());
f.extend_from_slice(&(48_000u32 * 2).to_le_bytes());
f.extend_from_slice(&2u16.to_le_bytes());
f.extend_from_slice(&16u16.to_le_bytes());
f
};
let mut out: Vec<u8> = Vec::new();
out.extend_from_slice(b"RIFF");
out.extend_from_slice(&0u32.to_le_bytes());
out.extend_from_slice(b"WAVE");
out.extend_from_slice(b"fmt ");
out.extend_from_slice(&(fmt_body.len() as u32).to_le_bytes());
out.extend_from_slice(&fmt_body);
out.extend_from_slice(b"data");
out.extend_from_slice(&SIZE64_SENTINEL.to_le_bytes());
use std::io::Cursor;
let rs: Box<dyn ReadSeek> = Box::new(Cursor::new(out));
let res = open_demuxer(rs, &oxideav_core::NullCodecResolver);
assert!(res.is_err(), "sentinel without ds64 must be rejected");
}
#[test]
fn rf64_ds64_short_body_is_rejected() {
let mut out: Vec<u8> = Vec::new();
out.extend_from_slice(b"RF64");
out.extend_from_slice(&SIZE64_SENTINEL.to_le_bytes());
out.extend_from_slice(b"WAVE");
out.extend_from_slice(b"ds64");
out.extend_from_slice(&8u32.to_le_bytes()); out.extend_from_slice(&[0u8; 8]);
use std::io::Cursor;
let rs: Box<dyn ReadSeek> = Box::new(Cursor::new(out));
let res = open_demuxer(rs, &oxideav_core::NullCodecResolver);
assert!(res.is_err(), "ds64 < 28 bytes must be rejected");
}
}