1use oxideav_core::{
40 CodecId, CodecParameters, CodecResolver, Error, MediaType, Packet, Result, SampleFormat,
41 StreamInfo, TimeBase,
42};
43use oxideav_core::{ContainerRegistry, Demuxer, Muxer, ReadSeek, WriteSeek};
44use std::io::{Read, Seek, SeekFrom, Write};
45
46pub fn register(reg: &mut ContainerRegistry) {
47 reg.register_demuxer("wav", open_demuxer);
48 reg.register_muxer("wav", open_muxer);
49 reg.register_extension("wav", "wav");
50 reg.register_extension("wave", "wav");
51 reg.register_probe("wav", probe);
52}
53
54fn probe(p: &oxideav_core::ProbeData) -> u8 {
58 if p.buf.len() < 12 {
59 return 0;
60 }
61 let magic = &p.buf[0..4];
62 let is_known_form = magic == b"RIFF" || magic == b"RF64" || magic == b"BW64";
63 if is_known_form && &p.buf[8..12] == b"WAVE" {
64 100
65 } else {
66 0
67 }
68}
69
70const SIZE64_SENTINEL: u32 = 0xFFFF_FFFF;
75
76#[derive(Default)]
96struct Ds64 {
97 #[allow(dead_code)]
103 riff_size: u64,
104 data_size: u64,
105 sample_count: u64,
106 table: Vec<([u8; 4], u64)>,
107}
108
109fn parse_ds64_chunk(buf: &[u8], out: &mut Vec<(String, String)>) -> Result<Ds64> {
120 if buf.len() < 28 {
121 return Err(Error::invalid("RF64 ds64 chunk shorter than 28 bytes"));
122 }
123 let riff_size = u64::from_le_bytes([
124 buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7],
125 ]);
126 let data_size = u64::from_le_bytes([
127 buf[8], buf[9], buf[10], buf[11], buf[12], buf[13], buf[14], buf[15],
128 ]);
129 let sample_count = u64::from_le_bytes([
130 buf[16], buf[17], buf[18], buf[19], buf[20], buf[21], buf[22], buf[23],
131 ]);
132 let table_len = u32::from_le_bytes([buf[24], buf[25], buf[26], buf[27]]) as usize;
133
134 out.push(("wav:rf64.riff_size".to_string(), riff_size.to_string()));
135 out.push(("wav:rf64.data_size".to_string(), data_size.to_string()));
136 out.push((
137 "wav:rf64.sample_count".to_string(),
138 sample_count.to_string(),
139 ));
140 out.push(("wav:rf64.table.count".to_string(), table_len.to_string()));
141
142 const REC_LEN: usize = 12;
143 let mut table = Vec::with_capacity(table_len);
144 let table_bytes_available = buf.len().saturating_sub(28);
145 let table_recs_available = table_bytes_available / REC_LEN;
146 let n = table_len.min(table_recs_available);
149 for i in 0..n {
150 let off = 28 + i * REC_LEN;
151 let id: [u8; 4] = [buf[off], buf[off + 1], buf[off + 2], buf[off + 3]];
152 let size = u64::from_le_bytes([
153 buf[off + 4],
154 buf[off + 5],
155 buf[off + 6],
156 buf[off + 7],
157 buf[off + 8],
158 buf[off + 9],
159 buf[off + 10],
160 buf[off + 11],
161 ]);
162 let id_str = if id.iter().all(|&b| (0x20..=0x7E).contains(&b)) {
163 String::from_utf8_lossy(&id).to_string()
164 } else {
165 format!("0x{:02X}{:02X}{:02X}{:02X}", id[0], id[1], id[2], id[3])
166 };
167 out.push((format!("wav:rf64.table.{i}.id"), id_str.clone()));
168 out.push((format!("wav:rf64.table.{i}.size"), size.to_string()));
169 table.push((id, size));
170 }
171 out.push(("wav:rf64.body_len".to_string(), buf.len().to_string()));
172 Ok(Ds64 {
173 riff_size,
174 data_size,
175 sample_count,
176 table,
177 })
178}
179
180fn resolve_chunk_size(id: &[u8; 4], on_wire: u32, ds64: Option<&Ds64>) -> Option<u64> {
187 if on_wire != SIZE64_SENTINEL {
188 return Some(on_wire as u64);
189 }
190 let ds64 = ds64?;
191 if id == b"data" {
194 return Some(ds64.data_size);
195 }
196 ds64.table
197 .iter()
198 .find(|(tid, _)| tid == id)
199 .map(|(_, sz)| *sz)
200}
201
202pub const WAVE_FORMAT_PCM: u16 = 0x0001;
207pub const WAVE_FORMAT_IEEE_FLOAT: u16 = 0x0003;
209pub const WAVE_FORMAT_ALAW: u16 = 0x0006;
211pub const WAVE_FORMAT_MULAW: u16 = 0x0007;
213pub const WAVE_FORMAT_EXTENSIBLE: u16 = 0xFFFE;
217
218const FMT_PCM: u16 = WAVE_FORMAT_PCM;
220const FMT_IEEE_FLOAT: u16 = WAVE_FORMAT_IEEE_FLOAT;
221const FMT_ALAW: u16 = WAVE_FORMAT_ALAW;
222const FMT_MULAW: u16 = WAVE_FORMAT_MULAW;
223const FMT_EXTENSIBLE: u16 = WAVE_FORMAT_EXTENSIBLE;
224
225const GUID_PCM: [u8; 16] = [
230 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71,
231];
232const GUID_IEEE_FLOAT: [u8; 16] = [
233 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71,
234];
235const GUID_ALAW: [u8; 16] = [
236 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71,
237];
238const GUID_MULAW: [u8; 16] = [
239 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71,
240];
241
242const GUID_WAVEFORMATEX_TAIL: [u8; 14] = [
259 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71,
260];
261
262fn waveformatex_tag(g: &[u8; 16]) -> Option<u16> {
275 if g[2..16] == GUID_WAVEFORMATEX_TAIL {
276 Some(u16::from_le_bytes([g[0], g[1]]))
277 } else {
278 None
279 }
280}
281
282fn fmt_guid(g: &[u8; 16]) -> String {
287 format!(
288 "{:08X}-{:04X}-{:04X}-{:02X}{:02X}-{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}",
289 u32::from_le_bytes([g[0], g[1], g[2], g[3]]),
290 u16::from_le_bytes([g[4], g[5]]),
291 u16::from_le_bytes([g[6], g[7]]),
292 g[8],
293 g[9],
294 g[10],
295 g[11],
296 g[12],
297 g[13],
298 g[14],
299 g[15],
300 )
301}
302
303const SPEAKER_FLAGS: [(u32, &str); 18] = [
311 (0x1, "FRONT_LEFT"),
312 (0x2, "FRONT_RIGHT"),
313 (0x4, "FRONT_CENTER"),
314 (0x8, "LOW_FREQUENCY"),
315 (0x10, "BACK_LEFT"),
316 (0x20, "BACK_RIGHT"),
317 (0x40, "FRONT_LEFT_OF_CENTER"),
318 (0x80, "FRONT_RIGHT_OF_CENTER"),
319 (0x100, "BACK_CENTER"),
320 (0x200, "SIDE_LEFT"),
321 (0x400, "SIDE_RIGHT"),
322 (0x800, "TOP_CENTER"),
323 (0x1000, "TOP_FRONT_LEFT"),
324 (0x2000, "TOP_FRONT_CENTER"),
325 (0x4000, "TOP_FRONT_RIGHT"),
326 (0x8000, "TOP_BACK_LEFT"),
327 (0x10000, "TOP_BACK_CENTER"),
328 (0x20000, "TOP_BACK_RIGHT"),
329];
330
331fn channel_mask_layout(mask: u32) -> Option<String> {
346 if mask == 0 {
347 return None;
348 }
349 let mut parts: Vec<String> = Vec::new();
350 let mut known: u32 = 0;
351 for (bit, name) in SPEAKER_FLAGS {
352 if mask & bit != 0 {
353 parts.push(name.to_string());
354 known |= bit;
355 }
356 }
357 let unknown = mask & !known;
358 if unknown != 0 {
359 parts.push(format!("UNKNOWN(0x{unknown:X})"));
360 }
361 Some(parts.join("+"))
362}
363
364fn open_demuxer(input: Box<dyn ReadSeek>, _codecs: &dyn CodecResolver) -> Result<Box<dyn Demuxer>> {
367 Ok(Box::new(open_wav_demuxer(input)?))
368}
369
370pub fn open_wav_demuxer(mut input: Box<dyn ReadSeek>) -> Result<WavDemuxer> {
376 let mut hdr = [0u8; 12];
377 input.read_exact(&mut hdr)?;
378 let magic: [u8; 4] = [hdr[0], hdr[1], hdr[2], hdr[3]];
379 let is_rf64 = &magic == b"RF64";
380 let is_bw64 = &magic == b"BW64";
381 let is_riff = &magic == b"RIFF";
382 if !(is_riff || is_rf64 || is_bw64) || &hdr[8..12] != b"WAVE" {
383 return Err(Error::invalid("not a RIFF/RF64/BW64 WAVE file"));
384 }
385
386 let mut fmt: Option<WaveFmt> = None;
389 let mut metadata: Vec<(String, String)> = Vec::new();
390 let mut fact_sample_count: Option<u64> = None;
396 let mut acid: Option<AcidChunk> = None;
398 let mut ds64: Option<Ds64> = None;
402 if is_rf64 || is_bw64 {
403 let form = if is_rf64 { "RF64" } else { "BW64" };
407 metadata.push(("wav:rf64.magic".to_string(), form.to_string()));
408 let on_wire_riff = u32::from_le_bytes([hdr[4], hdr[5], hdr[6], hdr[7]]);
414 if on_wire_riff != SIZE64_SENTINEL {
415 metadata.push(("wav:rf64.riff_size32".to_string(), on_wire_riff.to_string()));
416 }
417 }
418 let mut data_offset: Option<u64> = None;
419 let mut data_size: Option<u64> = None;
420 loop {
421 let mut chdr = [0u8; 8];
422 match input.read_exact(&mut chdr) {
428 Ok(()) => {}
429 Err(ref e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
430 Err(e) => return Err(e.into()),
431 }
432 let id_arr: [u8; 4] = [chdr[0], chdr[1], chdr[2], chdr[3]];
433 let id = &chdr[0..4];
434 let on_wire_size = u32::from_le_bytes([chdr[4], chdr[5], chdr[6], chdr[7]]);
435 let size = match resolve_chunk_size(&id_arr, on_wire_size, ds64.as_ref()) {
440 Some(s) => s,
441 None => {
442 return Err(Error::invalid(format!(
443 "RF64 chunk {:?} carries 0xFFFFFFFF sentinel but no ds64 override",
444 String::from_utf8_lossy(id)
445 )));
446 }
447 };
448 match id {
449 b"ds64" => {
450 let mut buf = vec![0u8; size as usize];
455 input.read_exact(&mut buf)?;
456 let parsed = parse_ds64_chunk(&buf, &mut metadata)?;
457 ds64 = Some(parsed);
458 if size % 2 == 1 {
459 input.seek(SeekFrom::Current(1))?;
460 }
461 }
462 b"fmt " => {
463 let mut buf = vec![0u8; size as usize];
464 input.read_exact(&mut buf)?;
465 fmt = Some(parse_fmt(&buf)?);
466 if size % 2 == 1 {
467 input.seek(SeekFrom::Current(1))?;
468 }
469 }
470 b"fact" => {
471 let mut buf = vec![0u8; size as usize];
472 input.read_exact(&mut buf)?;
473 let legacy = parse_fact_chunk(&buf, &mut metadata);
480 fact_sample_count = match legacy {
481 Some(v) if v == SIZE64_SENTINEL => ds64.as_ref().map(|d| d.sample_count),
482 Some(v) => Some(v as u64),
483 None => None,
484 };
485 if size % 2 == 1 {
486 input.seek(SeekFrom::Current(1))?;
487 }
488 }
489 b"LIST" => {
490 let list_start = input.stream_position()?;
502 let mut buf = vec![0u8; size as usize];
503 input.read_exact(&mut buf)?;
504 if buf.len() >= 4 && &buf[0..4] == b"wavl" {
505 if let Some((off, sz)) = parse_wavl_list(&buf, list_start, &mut metadata) {
506 if data_offset.is_none() {
513 data_offset = Some(off);
514 data_size = Some(sz);
515 }
516 }
517 } else {
518 parse_list_chunk(&buf, &mut metadata);
519 }
520 if size % 2 == 1 {
521 input.seek(SeekFrom::Current(1))?;
522 }
523 }
524 b"bext" => {
525 let mut buf = vec![0u8; size as usize];
526 input.read_exact(&mut buf)?;
527 parse_bext_chunk(&buf, &mut metadata);
528 if size % 2 == 1 {
529 input.seek(SeekFrom::Current(1))?;
530 }
531 }
532 b"cue " => {
533 let mut buf = vec![0u8; size as usize];
534 input.read_exact(&mut buf)?;
535 parse_cue_chunk(&buf, &mut metadata);
536 if size % 2 == 1 {
537 input.seek(SeekFrom::Current(1))?;
538 }
539 }
540 b"plst" => {
541 let mut buf = vec![0u8; size as usize];
542 input.read_exact(&mut buf)?;
543 parse_plst_chunk(&buf, &mut metadata);
544 if size % 2 == 1 {
545 input.seek(SeekFrom::Current(1))?;
546 }
547 }
548 b"smpl" => {
549 let mut buf = vec![0u8; size as usize];
550 input.read_exact(&mut buf)?;
551 parse_smpl_chunk(&buf, &mut metadata);
552 if size % 2 == 1 {
553 input.seek(SeekFrom::Current(1))?;
554 }
555 }
556 b"inst" => {
557 let mut buf = vec![0u8; size as usize];
558 input.read_exact(&mut buf)?;
559 parse_inst_chunk(&buf, &mut metadata);
560 if size % 2 == 1 {
561 input.seek(SeekFrom::Current(1))?;
562 }
563 }
564 b"acid" => {
565 let mut buf = vec![0u8; size as usize];
566 input.read_exact(&mut buf)?;
567 acid = parse_acid_chunk(&buf, &mut metadata);
568 if size % 2 == 1 {
569 input.seek(SeekFrom::Current(1))?;
570 }
571 }
572 b"iXML" => {
573 let mut buf = vec![0u8; size as usize];
574 input.read_exact(&mut buf)?;
575 parse_ixml_chunk(&buf, &mut metadata);
576 if size % 2 == 1 {
577 input.seek(SeekFrom::Current(1))?;
578 }
579 }
580 b"axml" => {
581 let mut buf = vec![0u8; size as usize];
582 input.read_exact(&mut buf)?;
583 parse_axml_chunk(&buf, &mut metadata);
584 if size % 2 == 1 {
585 input.seek(SeekFrom::Current(1))?;
586 }
587 }
588 b"_PMX" => {
589 let mut buf = vec![0u8; size as usize];
590 input.read_exact(&mut buf)?;
591 parse_pmx_chunk(&buf, &mut metadata);
592 if size % 2 == 1 {
593 input.seek(SeekFrom::Current(1))?;
594 }
595 }
596 b"CSET" => {
597 let mut buf = vec![0u8; size as usize];
598 input.read_exact(&mut buf)?;
599 parse_cset_chunk(&buf, &mut metadata);
600 if size % 2 == 1 {
601 input.seek(SeekFrom::Current(1))?;
602 }
603 }
604 b"JUNK" => {
605 input.seek(SeekFrom::Current(size as i64))?;
614 surface_junk_metadata(&mut metadata, size);
615 if size % 2 == 1 {
616 input.seek(SeekFrom::Current(1))?;
617 }
618 }
619 b"slnt" => {
620 let mut buf = vec![0u8; size as usize];
629 input.read_exact(&mut buf)?;
630 surface_slnt_metadata(&mut metadata, &buf);
631 if size % 2 == 1 {
632 input.seek(SeekFrom::Current(1))?;
633 }
634 }
635 b"data" => {
636 data_offset = Some(input.stream_position()?);
637 data_size = Some(size);
638 break;
639 }
640 _ => {
641 let pad = size + (size % 2);
642 input.seek(SeekFrom::Current(pad as i64))?;
643 }
644 }
645 }
646 let fmt = fmt.ok_or_else(|| Error::invalid("WAV missing fmt chunk"))?;
647 let data_offset =
650 data_offset.ok_or_else(|| Error::invalid("WAV missing data / wavl waveform"))?;
651 let data_size = data_size.unwrap_or(0);
652
653 let codec_id = resolve_codec(&fmt)?;
654 let sample_fmt = decoded_sample_format(&codec_id);
662
663 let time_base = TimeBase::new(1, fmt.sample_rate as i64);
664 let block_align = fmt.block_align.max(1) as u64;
665 let block_samples = data_size / block_align;
666 let total_samples = if let Some(fc) = fact_sample_count {
674 if fc != block_samples {
675 metadata.push((
676 "wav:fact.mismatch".to_string(),
677 format!("block_samples={block_samples} fact_samples={fc}"),
678 ));
679 }
680 fc
681 } else {
682 block_samples
683 };
684 let duration_micros: i64 = if fmt.sample_rate > 0 {
685 (total_samples as i128 * 1_000_000 / fmt.sample_rate as i128) as i64
686 } else {
687 0
688 };
689
690 let mut params = CodecParameters::audio(codec_id);
691 params.tag = Some(oxideav_core::CodecTag::wave_format(fmt.format_tag));
692 params.channels = Some(fmt.channels);
693 params.sample_rate = Some(fmt.sample_rate);
694 params.sample_format = sample_fmt;
695 params.bit_rate = Some(8 * block_align * (fmt.sample_rate as u64));
699
700 if fmt.format_tag == FMT_EXTENSIBLE {
705 if let Some(valid) = fmt.valid_bits_per_sample {
706 metadata.push((
707 "wav:fmt.valid_bits_per_sample".to_string(),
708 valid.to_string(),
709 ));
710 }
711 if let Some(mask) = fmt.channel_mask {
712 metadata.push(("wav:fmt.channel_mask".to_string(), format!("0x{mask:08X}")));
713 if let Some(layout) = channel_mask_layout(mask) {
714 metadata.push(("wav:fmt.channel_layout".to_string(), layout));
715 }
716 }
717 if let Some(sub) = &fmt.subformat {
718 metadata.push(("wav:fmt.subformat".to_string(), fmt_guid(sub)));
719 if let Some(tag) = waveformatex_tag(sub) {
727 metadata.push(("wav:fmt.subformat_tag".to_string(), format!("0x{tag:04X}")));
728 }
729 }
730 }
731
732 let stream = StreamInfo {
733 index: 0,
734 time_base,
735 duration: Some(total_samples as i64),
736 start_time: Some(0),
737 params,
738 };
739
740 Ok(WavDemuxer {
741 input,
742 streams: vec![stream],
743 data_offset,
744 data_end: data_offset + data_size,
745 cursor: data_offset,
746 block_align,
747 chunk_frames: 1024,
748 samples_emitted: 0,
749 metadata,
750 duration_micros,
751 format_tag: fmt.format_tag,
752 valid_bits_per_sample: fmt.valid_bits_per_sample,
753 channel_mask: fmt.channel_mask,
754 subformat: fmt.subformat,
755 acid,
756 })
757}
758
759fn parse_list_chunk(buf: &[u8], out: &mut Vec<(String, String)>) {
767 if buf.len() < 4 {
768 return;
769 }
770 match &buf[0..4] {
771 b"INFO" => parse_info_list(&buf[4..], out),
772 b"adtl" => parse_adtl_list(&buf[4..], out),
773 _ => {}
774 }
775}
776
777fn parse_wavl_list(
809 buf: &[u8],
810 list_start: u64,
811 out: &mut Vec<(String, String)>,
812) -> Option<(u64, u64)> {
813 let mut i = 4usize;
815 let mut first_data: Option<(u64, u64)> = None;
816 let mut segment_count: u64 = 0;
817 let mut data_count: u64 = 0;
818 let mut data_bytes: u64 = 0;
819 while i + 8 <= buf.len() {
820 let id: [u8; 4] = [buf[i], buf[i + 1], buf[i + 2], buf[i + 3]];
821 let size = u32::from_le_bytes([buf[i + 4], buf[i + 5], buf[i + 6], buf[i + 7]]) as usize;
822 let body = i + 8;
823 if body + size > buf.len() {
824 break;
825 }
826 let idx = segment_count;
827 match &id {
828 b"data" => {
829 out.push((format!("wav:wavl.{idx}.kind"), "data".to_string()));
830 out.push((format!("wav:wavl.{idx}.length"), size.to_string()));
831 data_count += 1;
832 data_bytes = data_bytes.saturating_add(size as u64);
833 if first_data.is_none() {
834 first_data = Some((list_start + body as u64, size as u64));
837 }
838 }
839 b"slnt" => {
840 out.push((format!("wav:wavl.{idx}.kind"), "slnt".to_string()));
841 out.push((format!("wav:wavl.{idx}.length"), size.to_string()));
842 surface_slnt_metadata(out, &buf[body..body + size]);
843 }
844 _ => {
849 out.push((
850 format!("wav:wavl.{idx}.kind"),
851 String::from_utf8_lossy(&id).trim().to_string(),
852 ));
853 out.push((format!("wav:wavl.{idx}.length"), size.to_string()));
854 }
855 }
856 segment_count += 1;
857 i = body + size + (size & 1);
859 }
860 out.push((
861 "wav:wavl.segment_count".to_string(),
862 segment_count.to_string(),
863 ));
864 out.push(("wav:wavl.data_count".to_string(), data_count.to_string()));
865 out.push(("wav:wavl.data_bytes".to_string(), data_bytes.to_string()));
866 first_data
867}
868
869fn parse_info_list(buf: &[u8], out: &mut Vec<(String, String)>) {
870 let mut i = 0usize;
871 while i + 8 <= buf.len() {
872 let id: [u8; 4] = [buf[i], buf[i + 1], buf[i + 2], buf[i + 3]];
873 let size = u32::from_le_bytes([buf[i + 4], buf[i + 5], buf[i + 6], buf[i + 7]]) as usize;
874 i += 8;
875 if i + size > buf.len() {
876 break;
877 }
878 let raw = &buf[i..i + size];
879 let end = raw.iter().position(|&b| b == 0).unwrap_or(raw.len());
880 let value = String::from_utf8_lossy(&raw[..end]).trim().to_string();
881 let key = info_id_to_key(&id);
882 if !value.is_empty() {
883 if let Some(k) = key {
884 out.push((k.to_string(), value));
885 }
886 }
887 i += size;
888 if size % 2 == 1 {
889 i += 1;
890 }
891 }
892}
893
894fn parse_adtl_list(buf: &[u8], out: &mut Vec<(String, String)>) {
930 let mut i = 0usize;
931 while i + 8 <= buf.len() {
932 let id: [u8; 4] = [buf[i], buf[i + 1], buf[i + 2], buf[i + 3]];
933 let size = u32::from_le_bytes([buf[i + 4], buf[i + 5], buf[i + 6], buf[i + 7]]) as usize;
934 i += 8;
935 if i + size > buf.len() {
936 break;
937 }
938 let body = &buf[i..i + size];
939 match &id {
940 b"labl" | b"note" if body.len() >= 4 => {
941 let dw_name = u32::from_le_bytes([body[0], body[1], body[2], body[3]]);
942 let raw = &body[4..];
943 let end = raw.iter().position(|&b| b == 0).unwrap_or(raw.len());
944 let text = String::from_utf8_lossy(&raw[..end]).trim().to_string();
945 if !text.is_empty() {
946 let sub = if &id == b"labl" { "labl" } else { "note" };
947 out.push((format!("wav:adtl.{sub}.{dw_name}"), text));
948 }
949 }
950 b"ltxt" if body.len() >= 20 => {
954 let dw_name = u32::from_le_bytes([body[0], body[1], body[2], body[3]]);
955 let dw_length = u32::from_le_bytes([body[4], body[5], body[6], body[7]]);
956 let purpose = &body[8..12];
957 out.push((
958 format!("wav:adtl.ltxt.{dw_name}.length"),
959 dw_length.to_string(),
960 ));
961 let purpose_str = if purpose.iter().all(|&b| (0x20..=0x7E).contains(&b)) {
963 String::from_utf8_lossy(purpose).to_string()
964 } else {
965 format!(
966 "0x{:02X}{:02X}{:02X}{:02X}",
967 purpose[0], purpose[1], purpose[2], purpose[3]
968 )
969 };
970 out.push((format!("wav:adtl.ltxt.{dw_name}.purpose"), purpose_str));
971 let country = u16::from_le_bytes([body[12], body[13]]);
979 let language = u16::from_le_bytes([body[14], body[15]]);
980 let dialect = u16::from_le_bytes([body[16], body[17]]);
981 let code_page = u16::from_le_bytes([body[18], body[19]]);
982 out.push((
983 format!("wav:adtl.ltxt.{dw_name}.country"),
984 country.to_string(),
985 ));
986 if let Some(name) = cset_country_name(country) {
987 out.push((
988 format!("wav:adtl.ltxt.{dw_name}.country_name"),
989 name.to_string(),
990 ));
991 }
992 out.push((
993 format!("wav:adtl.ltxt.{dw_name}.language"),
994 language.to_string(),
995 ));
996 out.push((
997 format!("wav:adtl.ltxt.{dw_name}.dialect"),
998 dialect.to_string(),
999 ));
1000 if let Some(name) = cset_language_name(language, dialect) {
1001 out.push((
1002 format!("wav:adtl.ltxt.{dw_name}.language_name"),
1003 name.to_string(),
1004 ));
1005 }
1006 out.push((
1007 format!("wav:adtl.ltxt.{dw_name}.code_page"),
1008 code_page.to_string(),
1009 ));
1010 let raw = &body[20..];
1011 let end = raw.iter().position(|&b| b == 0).unwrap_or(raw.len());
1015 let text = String::from_utf8_lossy(&raw[..end]).trim().to_string();
1016 if !text.is_empty() {
1017 out.push((format!("wav:adtl.ltxt.{dw_name}.text"), text));
1018 }
1019 }
1020 b"file" if body.len() >= 8 => {
1025 let dw_name = u32::from_le_bytes([body[0], body[1], body[2], body[3]]);
1026 let med_type = &body[4..8];
1027 let med_type_str = if med_type == [0, 0, 0, 0] {
1031 "0".to_string()
1032 } else if med_type.iter().all(|&b| (0x20..=0x7E).contains(&b)) {
1033 String::from_utf8_lossy(med_type).to_string()
1034 } else {
1035 format!(
1036 "0x{:02X}{:02X}{:02X}{:02X}",
1037 med_type[0], med_type[1], med_type[2], med_type[3]
1038 )
1039 };
1040 out.push((format!("wav:adtl.file.{dw_name}.med_type"), med_type_str));
1041 out.push((
1042 format!("wav:adtl.file.{dw_name}.body_len"),
1043 (body.len() - 8).to_string(),
1044 ));
1045 }
1046 _ => {}
1049 }
1050 i += size;
1051 if size % 2 == 1 {
1052 i += 1;
1053 }
1054 }
1055}
1056
1057fn parse_cue_chunk(buf: &[u8], out: &mut Vec<(String, String)>) {
1080 if buf.len() < 4 {
1081 return;
1082 }
1083 let count_claimed = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
1084 let body = &buf[4..];
1085 const REC_LEN: usize = 24;
1086 let count_actual = (body.len() / REC_LEN) as u32;
1087 let count = count_claimed.min(count_actual);
1088 out.push(("wav:cue.count".to_string(), count.to_string()));
1089 for i in 0..count as usize {
1090 let off = i * REC_LEN;
1091 let dw_name = u32::from_le_bytes([body[off], body[off + 1], body[off + 2], body[off + 3]]);
1092 let dw_position =
1093 u32::from_le_bytes([body[off + 4], body[off + 5], body[off + 6], body[off + 7]]);
1094 let fcc_chunk = &body[off + 8..off + 12];
1095 let dw_chunk_start = u32::from_le_bytes([
1096 body[off + 12],
1097 body[off + 13],
1098 body[off + 14],
1099 body[off + 15],
1100 ]);
1101 let dw_block_start = u32::from_le_bytes([
1102 body[off + 16],
1103 body[off + 17],
1104 body[off + 18],
1105 body[off + 19],
1106 ]);
1107 let dw_sample_offset = u32::from_le_bytes([
1108 body[off + 20],
1109 body[off + 21],
1110 body[off + 22],
1111 body[off + 23],
1112 ]);
1113 let fcc_str = if fcc_chunk.iter().all(|&b| (0x20..=0x7E).contains(&b)) {
1114 String::from_utf8_lossy(fcc_chunk).to_string()
1115 } else {
1116 format!(
1117 "0x{:02X}{:02X}{:02X}{:02X}",
1118 fcc_chunk[0], fcc_chunk[1], fcc_chunk[2], fcc_chunk[3]
1119 )
1120 };
1121 out.push((
1122 format!("wav:cue.{dw_name}.position"),
1123 dw_position.to_string(),
1124 ));
1125 out.push((format!("wav:cue.{dw_name}.fcc_chunk"), fcc_str));
1126 out.push((
1127 format!("wav:cue.{dw_name}.chunk_start"),
1128 dw_chunk_start.to_string(),
1129 ));
1130 out.push((
1131 format!("wav:cue.{dw_name}.block_start"),
1132 dw_block_start.to_string(),
1133 ));
1134 out.push((
1135 format!("wav:cue.{dw_name}.sample_offset"),
1136 dw_sample_offset.to_string(),
1137 ));
1138 }
1139}
1140
1141fn parse_plst_chunk(buf: &[u8], out: &mut Vec<(String, String)>) {
1164 if buf.len() < 4 {
1165 return;
1166 }
1167 let count_claimed = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
1168 let body = &buf[4..];
1169 const REC_LEN: usize = 12;
1170 let count_actual = (body.len() / REC_LEN) as u32;
1171 let count = count_claimed.min(count_actual);
1172 out.push(("wav:plst.count".to_string(), count.to_string()));
1173 for i in 0..count as usize {
1174 let off = i * REC_LEN;
1175 let dw_name = u32::from_le_bytes([body[off], body[off + 1], body[off + 2], body[off + 3]]);
1176 let dw_length =
1177 u32::from_le_bytes([body[off + 4], body[off + 5], body[off + 6], body[off + 7]]);
1178 let dw_loops =
1179 u32::from_le_bytes([body[off + 8], body[off + 9], body[off + 10], body[off + 11]]);
1180 out.push((format!("wav:plst.{i}.cue_id"), dw_name.to_string()));
1181 out.push((format!("wav:plst.{i}.length"), dw_length.to_string()));
1182 out.push((format!("wav:plst.{i}.loops"), dw_loops.to_string()));
1183 }
1184}
1185
1186fn parse_smpl_chunk(buf: &[u8], out: &mut Vec<(String, String)>) {
1222 const FIXED_LEN: usize = 36;
1223 const LOOP_LEN: usize = 24;
1224 if buf.len() < FIXED_LEN {
1225 return;
1226 }
1227 let r = |off: usize| -> u32 {
1228 u32::from_le_bytes([buf[off], buf[off + 1], buf[off + 2], buf[off + 3]])
1229 };
1230 let manufacturer = r(0);
1231 let product = r(4);
1232 let sample_period = r(8);
1233 let midi_unity_note = r(12);
1234 let midi_pitch_fraction = r(16);
1235 let smpte_format = r(20);
1236 let smpte_offset = r(24);
1237 let num_loops_claimed = r(28);
1238 let sampler_data_len = r(32);
1239
1240 out.push((
1241 "wav:smpl.manufacturer".to_string(),
1242 manufacturer.to_string(),
1243 ));
1244 out.push(("wav:smpl.product".to_string(), product.to_string()));
1245 out.push((
1246 "wav:smpl.sample_period".to_string(),
1247 sample_period.to_string(),
1248 ));
1249 out.push((
1250 "wav:smpl.midi_unity_note".to_string(),
1251 midi_unity_note.to_string(),
1252 ));
1253 out.push((
1254 "wav:smpl.midi_pitch_fraction".to_string(),
1255 midi_pitch_fraction.to_string(),
1256 ));
1257 out.push((
1258 "wav:smpl.smpte_format".to_string(),
1259 smpte_format.to_string(),
1260 ));
1261 let smpte_hh = (smpte_offset >> 24) & 0xFF;
1265 let smpte_mm = (smpte_offset >> 16) & 0xFF;
1266 let smpte_ss = (smpte_offset >> 8) & 0xFF;
1267 let smpte_ff = smpte_offset & 0xFF;
1268 out.push((
1269 "wav:smpl.smpte_offset".to_string(),
1270 format!("{smpte_hh:02}:{smpte_mm:02}:{smpte_ss:02}:{smpte_ff:02}"),
1271 ));
1272 out.push((
1273 "wav:smpl.sampler_data_len".to_string(),
1274 sampler_data_len.to_string(),
1275 ));
1276
1277 let body = &buf[FIXED_LEN..];
1278 let num_loops_fits = (body.len() / LOOP_LEN) as u32;
1279 let num_loops = num_loops_claimed.min(num_loops_fits);
1280 out.push((
1281 "wav:smpl.num_sample_loops".to_string(),
1282 num_loops.to_string(),
1283 ));
1284 for i in 0..num_loops as usize {
1285 let off = i * LOOP_LEN;
1286 let loop_field = |word: usize| -> u32 {
1287 let p = off + word * 4;
1288 u32::from_le_bytes([body[p], body[p + 1], body[p + 2], body[p + 3]])
1289 };
1290 let cue_point_id = loop_field(0);
1291 let loop_type = loop_field(1);
1292 let start = loop_field(2);
1293 let end = loop_field(3);
1294 let fraction = loop_field(4);
1295 let play_count = loop_field(5);
1296 out.push((
1297 format!("wav:smpl.loop.{i}.cue_point_id"),
1298 cue_point_id.to_string(),
1299 ));
1300 out.push((format!("wav:smpl.loop.{i}.type"), loop_type.to_string()));
1301 out.push((format!("wav:smpl.loop.{i}.start"), start.to_string()));
1302 out.push((format!("wav:smpl.loop.{i}.end"), end.to_string()));
1303 out.push((format!("wav:smpl.loop.{i}.fraction"), fraction.to_string()));
1304 out.push((
1305 format!("wav:smpl.loop.{i}.play_count"),
1306 play_count.to_string(),
1307 ));
1308 }
1309}
1310
1311fn parse_inst_chunk(buf: &[u8], out: &mut Vec<(String, String)>) {
1329 const FIXED_LEN: usize = 7;
1330 if buf.len() < FIXED_LEN {
1331 return;
1332 }
1333 let unshifted_note = buf[0];
1334 let fine_tune = buf[1] as i8;
1335 let gain = buf[2] as i8;
1336 let low_note = buf[3];
1337 let high_note = buf[4];
1338 let low_velocity = buf[5];
1339 let high_velocity = buf[6];
1340 out.push((
1341 "wav:inst.unshifted_note".to_string(),
1342 unshifted_note.to_string(),
1343 ));
1344 out.push(("wav:inst.fine_tune".to_string(), fine_tune.to_string()));
1345 out.push(("wav:inst.gain".to_string(), gain.to_string()));
1346 out.push(("wav:inst.low_note".to_string(), low_note.to_string()));
1347 out.push(("wav:inst.high_note".to_string(), high_note.to_string()));
1348 out.push((
1349 "wav:inst.low_velocity".to_string(),
1350 low_velocity.to_string(),
1351 ));
1352 out.push((
1353 "wav:inst.high_velocity".to_string(),
1354 high_velocity.to_string(),
1355 ));
1356}
1357
1358pub const ACID_FLAG_ONE_SHOT: u32 = 1 << 0;
1361pub const ACID_FLAG_ROOT_NOTE_SET: u32 = 1 << 1;
1363pub const ACID_FLAG_STRETCH: u32 = 1 << 2;
1365pub const ACID_FLAG_DISK_BASED: u32 = 1 << 3;
1368pub const ACID_FLAG_HIGH_OCTAVE: u32 = 1 << 4;
1370
1371#[derive(Clone, Copy, Debug, PartialEq)]
1395pub struct AcidChunk {
1396 pub flags: u32,
1398 pub root_note: u16,
1401 pub reserved: [u8; 6],
1404 pub num_beats: u32,
1406 pub meter: u32,
1409 pub tempo: f32,
1411}
1412
1413impl AcidChunk {
1414 pub const BODY_LEN: usize = 24;
1416
1417 pub fn one_shot(&self) -> bool {
1419 self.flags & ACID_FLAG_ONE_SHOT != 0
1420 }
1421
1422 pub fn root_note_set(&self) -> bool {
1424 self.flags & ACID_FLAG_ROOT_NOTE_SET != 0
1425 }
1426
1427 pub fn stretch(&self) -> bool {
1429 self.flags & ACID_FLAG_STRETCH != 0
1430 }
1431
1432 pub fn disk_based(&self) -> bool {
1434 self.flags & ACID_FLAG_DISK_BASED != 0
1435 }
1436
1437 pub fn high_octave(&self) -> bool {
1439 self.flags & ACID_FLAG_HIGH_OCTAVE != 0
1440 }
1441
1442 pub fn root_note_name(&self) -> Option<&'static str> {
1446 const NAMES: [&str; 24] = [
1447 "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B", "High C", "High C#",
1448 "High D", "High D#", "High E", "High F", "High F#", "High G", "High G#", "High A",
1449 "High A#", "High B",
1450 ];
1451 NAMES.get(self.root_note.wrapping_sub(48) as usize).copied()
1452 }
1453
1454 pub fn parse(buf: &[u8]) -> Option<AcidChunk> {
1459 if buf.len() < Self::BODY_LEN {
1460 return None;
1461 }
1462 let r32 =
1463 |o: usize| -> u32 { u32::from_le_bytes([buf[o], buf[o + 1], buf[o + 2], buf[o + 3]]) };
1464 let mut reserved = [0u8; 6];
1465 reserved.copy_from_slice(&buf[6..12]);
1466 Some(AcidChunk {
1467 flags: r32(0),
1468 root_note: u16::from_le_bytes([buf[4], buf[5]]),
1469 reserved,
1470 num_beats: r32(12),
1471 meter: r32(16),
1472 tempo: f32::from_le_bytes([buf[20], buf[21], buf[22], buf[23]]),
1473 })
1474 }
1475
1476 pub fn to_bytes(&self) -> [u8; 24] {
1479 let mut out = [0u8; 24];
1480 out[0..4].copy_from_slice(&self.flags.to_le_bytes());
1481 out[4..6].copy_from_slice(&self.root_note.to_le_bytes());
1482 out[6..12].copy_from_slice(&self.reserved);
1483 out[12..16].copy_from_slice(&self.num_beats.to_le_bytes());
1484 out[16..20].copy_from_slice(&self.meter.to_le_bytes());
1485 out[20..24].copy_from_slice(&self.tempo.to_le_bytes());
1486 out
1487 }
1488}
1489
1490fn parse_acid_chunk(buf: &[u8], out: &mut Vec<(String, String)>) -> Option<AcidChunk> {
1505 let acid = AcidChunk::parse(buf)?;
1506 out.push((
1507 "wav:acid.flags".to_string(),
1508 format!("0x{:08X}", acid.flags),
1509 ));
1510 out.push((
1511 "wav:acid.one_shot".to_string(),
1512 (acid.one_shot() as u8).to_string(),
1513 ));
1514 out.push((
1515 "wav:acid.root_note_set".to_string(),
1516 (acid.root_note_set() as u8).to_string(),
1517 ));
1518 out.push((
1519 "wav:acid.stretch".to_string(),
1520 (acid.stretch() as u8).to_string(),
1521 ));
1522 out.push((
1523 "wav:acid.disk_based".to_string(),
1524 (acid.disk_based() as u8).to_string(),
1525 ));
1526 out.push((
1527 "wav:acid.high_octave".to_string(),
1528 (acid.high_octave() as u8).to_string(),
1529 ));
1530 out.push(("wav:acid.root_note".to_string(), acid.root_note.to_string()));
1531 if let Some(name) = acid.root_note_name() {
1532 out.push(("wav:acid.root_note_name".to_string(), name.to_string()));
1533 }
1534 out.push(("wav:acid.num_beats".to_string(), acid.num_beats.to_string()));
1535 out.push(("wav:acid.meter".to_string(), acid.meter.to_string()));
1536 out.push(("wav:acid.tempo".to_string(), acid.tempo.to_string()));
1537 if acid.reserved.iter().any(|&b| b != 0) {
1538 let hex: String = acid.reserved.iter().map(|b| format!("{b:02X}")).collect();
1539 out.push(("wav:acid.reserved".to_string(), hex));
1540 }
1541 if buf.len() > AcidChunk::BODY_LEN {
1542 out.push(("wav:acid.body_len".to_string(), buf.len().to_string()));
1543 }
1544 Some(acid)
1545}
1546
1547fn parse_fact_chunk(buf: &[u8], out: &mut Vec<(String, String)>) -> Option<u32> {
1577 if buf.len() < 4 {
1578 return None;
1579 }
1580 let sample_count = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
1581 out.push((
1582 "wav:fact.sample_count".to_string(),
1583 sample_count.to_string(),
1584 ));
1585 if buf.len() > 4 {
1586 out.push(("wav:fact.body_len".to_string(), buf.len().to_string()));
1587 }
1588 Some(sample_count)
1589}
1590
1591fn parse_ixml_chunk(buf: &[u8], out: &mut Vec<(String, String)>) {
1617 out.push(("wav:ixml.body_len".to_string(), buf.len().to_string()));
1618 let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
1619 let text = String::from_utf8_lossy(&buf[..end]).trim().to_string();
1620 if !text.is_empty() {
1621 out.push(("wav:ixml".to_string(), text));
1622 }
1623}
1624
1625fn parse_axml_chunk(buf: &[u8], out: &mut Vec<(String, String)>) {
1674 out.push(("wav:axml.body_len".to_string(), buf.len().to_string()));
1675 let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
1676 let text = String::from_utf8_lossy(&buf[..end]).trim().to_string();
1677 if !text.is_empty() {
1678 out.push(("wav:axml".to_string(), text));
1679 }
1680}
1681
1682fn parse_pmx_chunk(buf: &[u8], out: &mut Vec<(String, String)>) {
1721 out.push(("wav:xmp.body_len".to_string(), buf.len().to_string()));
1722 let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
1723 let text = String::from_utf8_lossy(&buf[..end]).trim().to_string();
1724 if !text.is_empty() {
1725 out.push(("wav:xmp".to_string(), text));
1726 }
1727}
1728
1729fn surface_junk_metadata(out: &mut Vec<(String, String)>, size: u64) {
1763 let mut count: u64 = 0;
1770 let mut total: u64 = 0;
1771 for (k, v) in out.iter() {
1772 if k == "wav:junk.count" {
1773 count = v.parse().unwrap_or(count);
1774 } else if k == "wav:junk.total_bytes" {
1775 total = v.parse().unwrap_or(total);
1776 }
1777 }
1778 let idx = count;
1779 count = count.saturating_add(1);
1780 total = total.saturating_add(size);
1781 out.push((format!("wav:junk.{idx}.body_len"), size.to_string()));
1783 out.retain(|(k, _)| k != "wav:junk.count" && k != "wav:junk.total_bytes");
1787 out.push(("wav:junk.count".to_string(), count.to_string()));
1788 out.push(("wav:junk.total_bytes".to_string(), total.to_string()));
1789}
1790
1791fn surface_slnt_metadata(out: &mut Vec<(String, String)>, buf: &[u8]) {
1828 let mut count: u64 = 0;
1829 let mut total: u64 = 0;
1830 for (k, v) in out.iter() {
1831 if k == "wav:slnt.count" {
1832 count = v.parse().unwrap_or(count);
1833 } else if k == "wav:slnt.total_samples" {
1834 total = v.parse().unwrap_or(total);
1835 }
1836 }
1837 let idx = count;
1838 count = count.saturating_add(1);
1839 if buf.len() >= 4 {
1840 let samples = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
1841 total = total.saturating_add(samples as u64);
1842 out.push((format!("wav:slnt.{idx}.samples"), samples.to_string()));
1843 }
1844 out.retain(|(k, _)| k != "wav:slnt.count" && k != "wav:slnt.total_samples");
1845 out.push(("wav:slnt.count".to_string(), count.to_string()));
1846 out.push(("wav:slnt.total_samples".to_string(), total.to_string()));
1847}
1848
1849fn cset_country_name(code: u16) -> Option<&'static str> {
1854 match code {
1858 0 => Some("None"),
1859 1 => Some("USA"),
1860 2 => Some("Canada"),
1861 3 => Some("Latin America"),
1862 30 => Some("Greece"),
1863 31 => Some("Netherlands"),
1864 32 => Some("Belgium"),
1865 33 => Some("France"),
1866 34 => Some("Spain"),
1867 39 => Some("Italy"),
1868 41 => Some("Switzerland"),
1869 43 => Some("Austria"),
1870 44 => Some("United Kingdom"),
1871 45 => Some("Denmark"),
1872 46 => Some("Sweden"),
1873 47 => Some("Norway"),
1874 49 => Some("West Germany"),
1875 52 => Some("Mexico"),
1876 55 => Some("Brazil"),
1877 61 => Some("Australia"),
1878 64 => Some("New Zealand"),
1879 81 => Some("Japan"),
1880 82 => Some("Korea"),
1881 86 => Some("People's Republic of China"),
1882 88 => Some("Taiwan"),
1883 90 => Some("Turkey"),
1884 351 => Some("Portugal"),
1885 352 => Some("Luxembourg"),
1886 354 => Some("Iceland"),
1887 358 => Some("Finland"),
1888 _ => None,
1889 }
1890}
1891
1892fn cset_language_name(language: u16, dialect: u16) -> Option<&'static str> {
1897 match (language, dialect) {
1904 (0, _) => Some("None"),
1905 (1, 1) => Some("Arabic"),
1906 (2, 1) => Some("Bulgarian"),
1907 (3, 1) => Some("Catalan"),
1908 (4, 1) => Some("Traditional Chinese"),
1909 (4, 2) => Some("Simplified Chinese"),
1910 (5, 1) => Some("Czech"),
1911 (6, 1) => Some("Danish"),
1912 (7, 1) => Some("German"),
1913 (7, 2) => Some("Swiss German"),
1914 (8, 1) => Some("Greek"),
1915 (9, 1) => Some("US English"),
1916 (9, 2) => Some("UK English"),
1917 (10, 1) => Some("Spanish"),
1918 (10, 2) => Some("Spanish Mexican"),
1919 (11, 1) => Some("Finnish"),
1920 (12, 1) => Some("French"),
1921 (12, 2) => Some("Belgian French"),
1922 (12, 3) => Some("Canadian French"),
1923 (12, 4) => Some("Swiss French"),
1924 (13, 1) => Some("Hebrew"),
1925 (14, 1) => Some("Hungarian"),
1926 (15, 1) => Some("Icelandic"),
1927 (16, 1) => Some("Italian"),
1928 (16, 2) => Some("Swiss Italian"),
1929 (17, 1) => Some("Japanese"),
1930 (18, 1) => Some("Korean"),
1931 (19, 1) => Some("Dutch"),
1932 (19, 2) => Some("Belgian Dutch"),
1933 (20, 1) => Some("Norwegian - Bokmal"),
1934 (20, 2) => Some("Norwegian - Nynorsk"),
1935 (21, 1) => Some("Polish"),
1936 (22, 1) => Some("Brazilian Portuguese"),
1937 (22, 2) => Some("Portuguese"),
1938 (23, 1) => Some("Rhaeto-Romanic"),
1939 (24, 1) => Some("Romanian"),
1940 (25, 1) => Some("Russian"),
1941 (26, 1) => Some("Serbo-Croatian (Latin)"),
1942 (26, 2) => Some("Serbo-Croatian (Cyrillic)"),
1943 (27, 1) => Some("Slovak"),
1944 (28, 1) => Some("Albanian"),
1945 (29, 1) => Some("Swedish"),
1946 (30, 1) => Some("Thai"),
1947 (31, 1) => Some("Turkish"),
1948 (32, 1) => Some("Urdu"),
1949 (33, 1) => Some("Bahasa"),
1950 _ => None,
1951 }
1952}
1953
1954fn parse_cset_chunk(buf: &[u8], out: &mut Vec<(String, String)>) {
2001 out.push(("wav:cset.body_len".to_string(), buf.len().to_string()));
2002 if buf.len() < 8 {
2003 return;
2004 }
2005 let code_page = u16::from_le_bytes([buf[0], buf[1]]);
2006 let country = u16::from_le_bytes([buf[2], buf[3]]);
2007 let language = u16::from_le_bytes([buf[4], buf[5]]);
2008 let dialect = u16::from_le_bytes([buf[6], buf[7]]);
2009 out.push(("wav:cset.code_page".to_string(), code_page.to_string()));
2010 out.push(("wav:cset.country".to_string(), country.to_string()));
2011 if let Some(name) = cset_country_name(country) {
2012 out.push(("wav:cset.country_name".to_string(), name.to_string()));
2013 }
2014 out.push(("wav:cset.language".to_string(), language.to_string()));
2015 out.push(("wav:cset.dialect".to_string(), dialect.to_string()));
2016 if let Some(name) = cset_language_name(language, dialect) {
2017 out.push(("wav:cset.language_name".to_string(), name.to_string()));
2018 }
2019}
2020
2021fn info_id_to_key(id: &[u8; 4]) -> Option<&'static str> {
2061 match id {
2062 b"IARL" => Some("archival_location"),
2064 b"IART" => Some("artist"),
2065 b"ICMS" => Some("commissioned"),
2066 b"ICMT" => Some("comment"),
2067 b"ICOP" => Some("copyright"),
2068 b"ICRD" => Some("date"),
2069 b"ICRP" => Some("cropped"),
2070 b"IDIM" => Some("dimensions"),
2071 b"IDPI" => Some("dpi"),
2072 b"IENG" => Some("engineer"),
2073 b"IGNR" => Some("genre"),
2074 b"IKEY" => Some("keywords"),
2075 b"ILGT" => Some("lightness"),
2076 b"IMED" => Some("medium"),
2077 b"INAM" => Some("title"),
2078 b"IPLT" => Some("palette_setting"),
2079 b"IPRD" => Some("album"),
2080 b"ISBJ" => Some("subject"),
2081 b"ISFT" => Some("encoder"),
2082 b"ISHP" => Some("sharpness"),
2083 b"ISRC" => Some("source"),
2084 b"ISRF" => Some("source_form"),
2085 b"ITCH" => Some("technician"),
2086 b"ITRK" => Some("track"),
2088 _ => None,
2089 }
2090}
2091
2092const BEXT_FIXED_LEN: usize = 602;
2101
2102fn bext_field(raw: &[u8]) -> String {
2106 let end = raw.iter().position(|&b| b == 0).unwrap_or(raw.len());
2107 String::from_utf8_lossy(&raw[..end]).trim().to_string()
2108}
2109
2110fn parse_bext_chunk(buf: &[u8], out: &mut Vec<(String, String)>) {
2127 if buf.len() < BEXT_FIXED_LEN {
2128 return;
2129 }
2130 let description = bext_field(&buf[0..256]);
2131 let originator = bext_field(&buf[256..288]);
2132 let originator_ref = bext_field(&buf[288..320]);
2133 let origination_date = bext_field(&buf[320..330]);
2134 let origination_time = bext_field(&buf[330..338]);
2135 let time_ref_low = u32::from_le_bytes([buf[338], buf[339], buf[340], buf[341]]);
2136 let time_ref_high = u32::from_le_bytes([buf[342], buf[343], buf[344], buf[345]]);
2137 let time_reference = ((time_ref_high as u64) << 32) | (time_ref_low as u64);
2138 let version = u16::from_le_bytes([buf[346], buf[347]]);
2139
2140 let umid = &buf[348..412];
2144
2145 let loudness_value = i16::from_le_bytes([buf[412], buf[413]]);
2147 let loudness_range = i16::from_le_bytes([buf[414], buf[415]]);
2148 let max_true_peak = i16::from_le_bytes([buf[416], buf[417]]);
2149 let max_momentary = i16::from_le_bytes([buf[418], buf[419]]);
2150 let max_short_term = i16::from_le_bytes([buf[420], buf[421]]);
2151
2152 let coding_history = if buf.len() > BEXT_FIXED_LEN {
2154 bext_field(&buf[BEXT_FIXED_LEN..])
2155 } else {
2156 String::new()
2157 };
2158
2159 let push = |out: &mut Vec<(String, String)>, key: &str, value: String| {
2160 if !value.is_empty() {
2161 out.push((key.to_string(), value));
2162 }
2163 };
2164
2165 push(out, "wav:bext.description", description);
2166 push(out, "wav:bext.originator", originator);
2167 push(out, "wav:bext.originator_reference", originator_ref);
2168 push(out, "wav:bext.origination_date", origination_date);
2169 push(out, "wav:bext.origination_time", origination_time);
2170 out.push((
2171 "wav:bext.time_reference".to_string(),
2172 time_reference.to_string(),
2173 ));
2174 out.push(("wav:bext.version".to_string(), version.to_string()));
2175
2176 if umid.iter().any(|&b| b != 0) {
2179 let mut hex = String::with_capacity(umid.len() * 2);
2180 for b in umid {
2181 hex.push_str(&format!("{b:02x}"));
2182 }
2183 out.push(("wav:bext.umid".to_string(), hex));
2184 }
2185
2186 if version >= 2 {
2189 out.push((
2190 "wav:bext.loudness_value".to_string(),
2191 fmt_loudness(loudness_value),
2192 ));
2193 out.push((
2194 "wav:bext.loudness_range".to_string(),
2195 fmt_loudness(loudness_range),
2196 ));
2197 out.push((
2198 "wav:bext.max_true_peak_level".to_string(),
2199 fmt_loudness(max_true_peak),
2200 ));
2201 out.push((
2202 "wav:bext.max_momentary_loudness".to_string(),
2203 fmt_loudness(max_momentary),
2204 ));
2205 out.push((
2206 "wav:bext.max_short_term_loudness".to_string(),
2207 fmt_loudness(max_short_term),
2208 ));
2209 }
2210
2211 push(out, "wav:bext.coding_history", coding_history);
2212}
2213
2214fn fmt_loudness(v: i16) -> String {
2219 let neg = v < 0;
2220 let abs = (v as i32).unsigned_abs();
2221 let whole = abs / 100;
2222 let frac = abs % 100;
2223 if neg {
2224 format!("-{whole}.{frac:02}")
2225 } else {
2226 format!("{whole}.{frac:02}")
2227 }
2228}
2229
2230#[derive(Clone, Debug)]
2231struct WaveFmt {
2232 format_tag: u16,
2233 channels: u16,
2234 sample_rate: u32,
2235 #[allow(dead_code)]
2236 byte_rate: u32,
2237 block_align: u16,
2238 bits_per_sample: u16,
2239 valid_bits_per_sample: Option<u16>,
2243 channel_mask: Option<u32>,
2245 subformat: Option<[u8; 16]>,
2247}
2248
2249fn parse_fmt(buf: &[u8]) -> Result<WaveFmt> {
2250 if buf.len() < 16 {
2251 return Err(Error::invalid("fmt chunk too small"));
2252 }
2253 let format_tag = u16::from_le_bytes([buf[0], buf[1]]);
2254 let channels = u16::from_le_bytes([buf[2], buf[3]]);
2255 let sample_rate = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]);
2256 let byte_rate = u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]);
2257 let block_align = u16::from_le_bytes([buf[12], buf[13]]);
2258 let bits_per_sample = u16::from_le_bytes([buf[14], buf[15]]);
2259 let mut valid_bits_per_sample = None;
2260 let mut channel_mask = None;
2261 let mut subformat = None;
2262 if format_tag == FMT_EXTENSIBLE {
2263 if buf.len() < 40 {
2271 return Err(Error::invalid(
2272 "WAVE_FORMAT_EXTENSIBLE fmt chunk shorter than the 40 bytes \
2273 mandated by mmreg.h",
2274 ));
2275 }
2276 let cb_size = u16::from_le_bytes([buf[16], buf[17]]);
2277 if (cb_size as usize) < 22 {
2278 return Err(Error::invalid(format!(
2279 "WAVE_FORMAT_EXTENSIBLE cbSize must be >= 22, got {cb_size}"
2280 )));
2281 }
2282 valid_bits_per_sample = Some(u16::from_le_bytes([buf[18], buf[19]]));
2283 channel_mask = Some(u32::from_le_bytes([buf[20], buf[21], buf[22], buf[23]]));
2284 let mut g = [0u8; 16];
2285 g.copy_from_slice(&buf[24..40]);
2286 subformat = Some(g);
2287 }
2288 Ok(WaveFmt {
2289 format_tag,
2290 channels,
2291 sample_rate,
2292 byte_rate,
2293 block_align,
2294 bits_per_sample,
2295 valid_bits_per_sample,
2296 channel_mask,
2297 subformat,
2298 })
2299}
2300
2301fn codec_for_tag(tag: u16, bits: u16) -> Result<Option<CodecId>> {
2312 Ok(match tag {
2313 FMT_PCM => Some(CodecId::new(pcm_int_codec(bits)?)),
2314 FMT_IEEE_FLOAT => Some(CodecId::new(pcm_float_codec(bits)?)),
2315 FMT_ALAW => Some(CodecId::new("pcm_alaw")),
2316 FMT_MULAW => Some(CodecId::new("pcm_mulaw")),
2317 _ => None,
2318 })
2319}
2320
2321fn resolve_codec(fmt: &WaveFmt) -> Result<CodecId> {
2322 if fmt.format_tag != FMT_EXTENSIBLE {
2323 return match codec_for_tag(fmt.format_tag, fmt.bits_per_sample)? {
2324 Some(id) => Ok(id),
2325 None => Err(Error::unsupported(format!(
2326 "unsupported WAV format tag 0x{:04x}",
2327 fmt.format_tag
2328 ))),
2329 };
2330 }
2331
2332 let sub = fmt
2333 .subformat
2334 .ok_or_else(|| Error::invalid("extensible WAV missing subformat"))?;
2335 let depth = fmt
2342 .valid_bits_per_sample
2343 .filter(|&v| v > 0)
2344 .unwrap_or(fmt.bits_per_sample);
2345 if let Some(tag) = waveformatex_tag(&sub) {
2356 if tag != FMT_EXTENSIBLE {
2357 if let Some(id) = codec_for_tag(tag, depth)? {
2358 return Ok(id);
2359 }
2360 }
2361 }
2362 Ok(CodecId::new(format!("wav:guid_{}", fmt_guid(&sub))))
2367}
2368
2369fn pcm_int_codec(bits: u16) -> Result<&'static str> {
2370 Ok(match bits {
2371 8 => "pcm_u8",
2372 16 => "pcm_s16le",
2373 24 => "pcm_s24le",
2374 32 => "pcm_s32le",
2375 _ => {
2376 return Err(Error::unsupported(format!(
2377 "unsupported WAV integer-PCM bit depth: {bits}"
2378 )));
2379 }
2380 })
2381}
2382
2383fn pcm_float_codec(bits: u16) -> Result<&'static str> {
2384 Ok(match bits {
2385 32 => "pcm_f32le",
2386 64 => "pcm_f64le",
2387 _ => {
2388 return Err(Error::unsupported(format!(
2389 "unsupported WAV IEEE-float bit depth: {bits}"
2390 )));
2391 }
2392 })
2393}
2394
2395fn decoded_sample_format(id: &CodecId) -> Option<SampleFormat> {
2405 if let Some(fmt) = super::pcm::sample_format_for(id) {
2407 return Some(fmt);
2408 }
2409 match id.as_str() {
2410 "pcm_alaw" | "pcm_mulaw" => Some(SampleFormat::S16),
2414 _ => None,
2415 }
2416}
2417
2418pub struct WavDemuxer {
2426 input: Box<dyn ReadSeek>,
2427 streams: Vec<StreamInfo>,
2428 data_offset: u64,
2429 data_end: u64,
2430 cursor: u64,
2431 block_align: u64,
2432 chunk_frames: u64,
2433 samples_emitted: i64,
2434 metadata: Vec<(String, String)>,
2435 duration_micros: i64,
2436 format_tag: u16,
2437 valid_bits_per_sample: Option<u16>,
2438 channel_mask: Option<u32>,
2439 subformat: Option<[u8; 16]>,
2440 acid: Option<AcidChunk>,
2441}
2442
2443impl WavDemuxer {
2444 pub fn format_tag(&self) -> u16 {
2448 self.format_tag
2449 }
2450
2451 pub fn valid_bits_per_sample(&self) -> Option<u16> {
2455 self.valid_bits_per_sample
2456 }
2457
2458 pub fn channel_mask(&self) -> Option<u32> {
2465 self.channel_mask
2466 }
2467
2468 pub fn channel_layout(&self) -> Option<String> {
2479 self.channel_mask.and_then(channel_mask_layout)
2480 }
2481
2482 pub fn subformat(&self) -> Option<&[u8; 16]> {
2488 self.subformat.as_ref()
2489 }
2490
2491 pub fn subformat_text(&self) -> Option<String> {
2494 self.subformat.as_ref().map(fmt_guid)
2495 }
2496
2497 pub fn acid(&self) -> Option<&AcidChunk> {
2502 self.acid.as_ref()
2503 }
2504}
2505
2506impl Demuxer for WavDemuxer {
2507 fn format_name(&self) -> &str {
2508 "wav"
2509 }
2510
2511 fn streams(&self) -> &[StreamInfo] {
2512 &self.streams
2513 }
2514
2515 fn next_packet(&mut self) -> Result<Packet> {
2516 if self.cursor >= self.data_end {
2517 return Err(Error::Eof);
2518 }
2519 let remaining = self.data_end - self.cursor;
2520 let want_bytes = (self.chunk_frames * self.block_align).min(remaining);
2521 let want_bytes = (want_bytes / self.block_align) * self.block_align;
2522 if want_bytes == 0 {
2523 return Err(Error::Eof);
2524 }
2525
2526 self.input.seek(SeekFrom::Start(self.cursor))?;
2528 let mut buf = vec![0u8; want_bytes as usize];
2529 self.input.read_exact(&mut buf)?;
2530 self.cursor += want_bytes;
2531
2532 let stream = &self.streams[0];
2533 let frames = want_bytes / self.block_align;
2534 let pts = self.samples_emitted;
2535 self.samples_emitted += frames as i64;
2536
2537 let mut pkt = Packet::new(0, stream.time_base, buf);
2538 pkt.pts = Some(pts);
2539 pkt.dts = Some(pts);
2540 pkt.duration = Some(frames as i64);
2541 pkt.flags.keyframe = true;
2542 Ok(pkt)
2543 }
2544
2545 fn seek_to(&mut self, stream_index: u32, pts: i64) -> Result<i64> {
2546 if stream_index != 0 {
2547 return Err(Error::invalid(format!(
2548 "WAV: stream index {stream_index} out of range"
2549 )));
2550 }
2551 let total_samples = (self.data_end - self.data_offset) / self.block_align;
2555 let target = (pts.max(0) as u64).min(total_samples);
2556 let new_cursor = self.data_offset + target * self.block_align;
2557
2558 self.input.seek(SeekFrom::Start(new_cursor))?;
2559 self.cursor = new_cursor;
2560 self.samples_emitted = target as i64;
2561 Ok(target as i64)
2562 }
2563
2564 fn metadata(&self) -> &[(String, String)] {
2565 &self.metadata
2566 }
2567
2568 fn duration_micros(&self) -> Option<i64> {
2569 if self.duration_micros > 0 {
2570 Some(self.duration_micros)
2571 } else {
2572 None
2573 }
2574 }
2575}
2576
2577fn open_muxer(output: Box<dyn WriteSeek>, streams: &[StreamInfo]) -> Result<Box<dyn Muxer>> {
2580 if streams.len() != 1 {
2581 return Err(Error::unsupported("WAV supports exactly one audio stream"));
2582 }
2583 let s = &streams[0];
2584 if s.params.media_type != MediaType::Audio {
2585 return Err(Error::invalid("WAV stream must be audio"));
2586 }
2587 let channels = s
2588 .params
2589 .channels
2590 .ok_or_else(|| Error::invalid("WAV muxer: missing channels"))?;
2591 let sample_rate = s
2592 .params
2593 .sample_rate
2594 .ok_or_else(|| Error::invalid("WAV muxer: missing sample rate"))?;
2595 let shape = wire_shape_for_params(&s.params)?;
2602 Ok(Box::new(WavMuxer {
2603 output,
2604 channels,
2605 sample_rate,
2606 shape,
2607 extensible: None,
2608 acid: None,
2609 riff_size_offset: 0,
2610 data_size_offset: 0,
2611 fact_size_offset: None,
2612 data_bytes: 0,
2613 header_written: false,
2614 trailer_written: false,
2615 }))
2616}
2617
2618#[derive(Clone, Debug, Default)]
2630pub struct WavMuxOptions {
2631 extensible: Option<ExtensibleOpts>,
2632 acid: Option<AcidChunk>,
2633}
2634
2635#[derive(Clone, Debug)]
2636struct ExtensibleOpts {
2637 channel_mask: u32,
2638 valid_bits_per_sample: Option<u16>,
2639 subformat: Option<[u8; 16]>,
2640}
2641
2642impl WavMuxOptions {
2643 pub fn with_extensible(mut self, channel_mask: u32) -> Self {
2648 self.extensible = Some(ExtensibleOpts {
2649 channel_mask,
2650 valid_bits_per_sample: None,
2651 subformat: None,
2652 });
2653 self
2654 }
2655
2656 pub fn with_valid_bits_per_sample(mut self, valid_bps: u16) -> Self {
2659 if let Some(opts) = self.extensible.as_mut() {
2660 opts.valid_bits_per_sample = Some(valid_bps);
2661 }
2662 self
2663 }
2664
2665 pub fn with_subformat(mut self, guid: [u8; 16]) -> Self {
2668 if let Some(opts) = self.extensible.as_mut() {
2669 opts.subformat = Some(guid);
2670 }
2671 self
2672 }
2673
2674 pub fn with_acid(mut self, acid: AcidChunk) -> Self {
2677 self.acid = Some(acid);
2678 self
2679 }
2680}
2681
2682pub fn open_muxer_with(
2686 output: Box<dyn WriteSeek>,
2687 streams: &[StreamInfo],
2688 opts: WavMuxOptions,
2689) -> Result<Box<dyn Muxer>> {
2690 if streams.len() != 1 {
2691 return Err(Error::unsupported("WAV supports exactly one audio stream"));
2692 }
2693 let s = &streams[0];
2694 if s.params.media_type != MediaType::Audio {
2695 return Err(Error::invalid("WAV stream must be audio"));
2696 }
2697 let channels = s
2698 .params
2699 .channels
2700 .ok_or_else(|| Error::invalid("WAV muxer: missing channels"))?;
2701 let sample_rate = s
2702 .params
2703 .sample_rate
2704 .ok_or_else(|| Error::invalid("WAV muxer: missing sample rate"))?;
2705 let shape = wire_shape_for_params(&s.params)?;
2706 Ok(Box::new(WavMuxer {
2707 output,
2708 channels,
2709 sample_rate,
2710 shape,
2711 extensible: opts.extensible,
2712 acid: opts.acid,
2713 riff_size_offset: 0,
2714 data_size_offset: 0,
2715 fact_size_offset: None,
2716 data_bytes: 0,
2717 header_written: false,
2718 trailer_written: false,
2719 }))
2720}
2721
2722#[derive(Clone, Copy, Debug)]
2726enum WireShape {
2727 IntPcm { bits: u16 },
2729 FloatPcm { bits: u16 },
2731 Alaw,
2733 Mulaw,
2735}
2736
2737impl WireShape {
2738 fn bits_per_sample(self) -> u16 {
2739 match self {
2740 WireShape::IntPcm { bits } | WireShape::FloatPcm { bits } => bits,
2741 WireShape::Alaw | WireShape::Mulaw => 8,
2742 }
2743 }
2744
2745 fn format_tag(self) -> u16 {
2746 match self {
2747 WireShape::IntPcm { .. } => FMT_PCM,
2748 WireShape::FloatPcm { .. } => FMT_IEEE_FLOAT,
2749 WireShape::Alaw => FMT_ALAW,
2750 WireShape::Mulaw => FMT_MULAW,
2751 }
2752 }
2753
2754 fn well_known_guid(self) -> [u8; 16] {
2755 match self {
2756 WireShape::IntPcm { .. } => GUID_PCM,
2757 WireShape::FloatPcm { .. } => GUID_IEEE_FLOAT,
2758 WireShape::Alaw => GUID_ALAW,
2759 WireShape::Mulaw => GUID_MULAW,
2760 }
2761 }
2762}
2763
2764fn wire_shape_for_params(p: &CodecParameters) -> Result<WireShape> {
2765 match p.codec_id.as_str() {
2766 "pcm_alaw" => return Ok(WireShape::Alaw),
2767 "pcm_mulaw" => return Ok(WireShape::Mulaw),
2768 _ => {}
2769 }
2770 let fmt = p
2771 .sample_format
2772 .or_else(|| super::pcm::sample_format_for(&p.codec_id))
2773 .ok_or_else(|| Error::unsupported(format!("WAV: unknown PCM codec {}", p.codec_id)))?;
2774 Ok(match fmt {
2775 SampleFormat::U8 => WireShape::IntPcm { bits: 8 },
2776 SampleFormat::S16 => WireShape::IntPcm { bits: 16 },
2777 SampleFormat::S24 => WireShape::IntPcm { bits: 24 },
2778 SampleFormat::S32 => WireShape::IntPcm { bits: 32 },
2779 SampleFormat::F32 => WireShape::FloatPcm { bits: 32 },
2780 SampleFormat::F64 => WireShape::FloatPcm { bits: 64 },
2781 other => {
2782 return Err(Error::unsupported(format!(
2783 "WAV muxer cannot write sample format {:?}",
2784 other
2785 )));
2786 }
2787 })
2788}
2789
2790struct WavMuxer {
2791 output: Box<dyn WriteSeek>,
2792 channels: u16,
2793 sample_rate: u32,
2794 shape: WireShape,
2795 extensible: Option<ExtensibleOpts>,
2796 acid: Option<AcidChunk>,
2799 riff_size_offset: u64,
2800 data_size_offset: u64,
2801 fact_size_offset: Option<u64>,
2809 data_bytes: u64,
2810 header_written: bool,
2811 trailer_written: bool,
2812}
2813
2814impl Muxer for WavMuxer {
2815 fn format_name(&self) -> &str {
2816 "wav"
2817 }
2818
2819 fn write_header(&mut self) -> Result<()> {
2820 if self.header_written {
2821 return Err(Error::other("WAV header already written"));
2822 }
2823 let bits_per_sample = self.shape.bits_per_sample();
2824 let block_align = (bits_per_sample / 8) * self.channels;
2825 let byte_rate = self.sample_rate * block_align as u32;
2826
2827 let format_tag = if self.extensible.is_some() {
2831 FMT_EXTENSIBLE
2832 } else {
2833 self.shape.format_tag()
2834 };
2835
2836 self.output.write_all(b"RIFF")?;
2837 self.riff_size_offset = self.output.stream_position()?;
2838 self.output.write_all(&0u32.to_le_bytes())?; self.output.write_all(b"WAVE")?;
2840
2841 let fmt_size: u32 = if self.extensible.is_some() { 40 } else { 16 };
2844 self.output.write_all(b"fmt ")?;
2845 self.output.write_all(&fmt_size.to_le_bytes())?;
2846 self.output.write_all(&format_tag.to_le_bytes())?;
2847 self.output.write_all(&self.channels.to_le_bytes())?;
2848 self.output.write_all(&self.sample_rate.to_le_bytes())?;
2849 self.output.write_all(&byte_rate.to_le_bytes())?;
2850 self.output.write_all(&block_align.to_le_bytes())?;
2851 self.output.write_all(&bits_per_sample.to_le_bytes())?;
2852
2853 if let Some(opts) = &self.extensible {
2854 self.output.write_all(&22u16.to_le_bytes())?;
2858 let valid = opts.valid_bits_per_sample.unwrap_or(bits_per_sample);
2859 self.output.write_all(&valid.to_le_bytes())?;
2860 self.output.write_all(&opts.channel_mask.to_le_bytes())?;
2861 let guid = opts
2862 .subformat
2863 .unwrap_or_else(|| self.shape.well_known_guid());
2864 self.output.write_all(&guid)?;
2865 }
2866
2867 if format_tag != FMT_PCM {
2874 self.output.write_all(b"fact")?;
2875 self.output.write_all(&4u32.to_le_bytes())?;
2876 self.fact_size_offset = Some(self.output.stream_position()?);
2877 self.output.write_all(&0u32.to_le_bytes())?; }
2879
2880 if let Some(acid) = &self.acid {
2884 self.output.write_all(b"acid")?;
2885 self.output
2886 .write_all(&(AcidChunk::BODY_LEN as u32).to_le_bytes())?;
2887 self.output.write_all(&acid.to_bytes())?;
2888 }
2889
2890 self.output.write_all(b"data")?;
2891 self.data_size_offset = self.output.stream_position()?;
2892 self.output.write_all(&0u32.to_le_bytes())?; self.header_written = true;
2895 Ok(())
2896 }
2897
2898 fn write_packet(&mut self, packet: &Packet) -> Result<()> {
2899 if !self.header_written {
2900 return Err(Error::other("WAV muxer: write_header not called"));
2901 }
2902 self.output.write_all(&packet.data)?;
2903 self.data_bytes += packet.data.len() as u64;
2904 Ok(())
2905 }
2906
2907 fn write_trailer(&mut self) -> Result<()> {
2908 if self.trailer_written {
2909 return Ok(());
2910 }
2911 if self.data_bytes % 2 == 1 {
2913 self.output.write_all(&[0u8])?;
2914 }
2915 let end = self.output.stream_position()?;
2916
2917 let data_size_u32: u32 = self
2919 .data_bytes
2920 .try_into()
2921 .map_err(|_| Error::other("WAV data chunk exceeds 4 GiB"))?;
2922 self.output.seek(SeekFrom::Start(self.data_size_offset))?;
2923 self.output.write_all(&data_size_u32.to_le_bytes())?;
2924
2925 if let Some(off) = self.fact_size_offset {
2933 let bits_per_sample = self.shape.bits_per_sample() as u64;
2934 let block_align = (bits_per_sample / 8) * self.channels as u64;
2935 let sample_count = self.data_bytes.checked_div(block_align).unwrap_or(0);
2936 let sample_count_u32: u32 = sample_count
2937 .try_into()
2938 .map_err(|_| Error::other("WAV fact sample count exceeds u32"))?;
2939 self.output.seek(SeekFrom::Start(off))?;
2940 self.output.write_all(&sample_count_u32.to_le_bytes())?;
2941 }
2942
2943 let riff_size_u32: u32 = (end - 8)
2945 .try_into()
2946 .map_err(|_| Error::other("WAV RIFF size exceeds 4 GiB"))?;
2947 self.output.seek(SeekFrom::Start(self.riff_size_offset))?;
2948 self.output.write_all(&riff_size_u32.to_le_bytes())?;
2949
2950 self.output.seek(SeekFrom::Start(end))?;
2951 self.output.flush()?;
2952 self.trailer_written = true;
2953 Ok(())
2954 }
2955}
2956
2957#[cfg(test)]
2958mod tests {
2959 use super::*;
2960 use oxideav_core::{CodecParameters, MediaType};
2961
2962 fn make_stream(fmt: SampleFormat, ch: u16, sr: u32) -> StreamInfo {
2963 let mut params = CodecParameters::audio(super::super::pcm::codec_id_for(fmt).unwrap());
2964 params.media_type = MediaType::Audio;
2965 params.channels = Some(ch);
2966 params.sample_rate = Some(sr);
2967 params.sample_format = Some(fmt);
2968 StreamInfo {
2969 index: 0,
2970 time_base: TimeBase::new(1, sr as i64),
2971 duration: None,
2972 start_time: Some(0),
2973 params,
2974 }
2975 }
2976
2977 fn wave_format_tag(p: &CodecParameters) -> Option<u16> {
2978 match p.tag.as_ref()? {
2979 oxideav_core::CodecTag::WaveFormat(t) => Some(*t),
2980 _ => None,
2981 }
2982 }
2983
2984 fn make_g711_stream(codec: &str, ch: u16, sr: u32) -> StreamInfo {
2985 let mut params = CodecParameters::audio(CodecId::new(codec));
2986 params.media_type = MediaType::Audio;
2987 params.channels = Some(ch);
2988 params.sample_rate = Some(sr);
2989 params.sample_format = Some(SampleFormat::S16);
2993 StreamInfo {
2994 index: 0,
2995 time_base: TimeBase::new(1, sr as i64),
2996 duration: None,
2997 start_time: Some(0),
2998 params,
2999 }
3000 }
3001
3002 fn mux_to_bytes(
3008 stream: &StreamInfo,
3009 payload: &[u8],
3010 opts: WavMuxOptions,
3011 tag: &str,
3012 ) -> Vec<u8> {
3013 let tmp = std::env::temp_dir().join(format!("oxideav-basic-wav-r77-{tag}.wav"));
3014 let _ = std::fs::remove_file(&tmp);
3015 {
3016 let f = std::fs::File::create(&tmp).unwrap();
3017 let ws: Box<dyn WriteSeek> = Box::new(f);
3018 let mut mux = open_muxer_with(ws, std::slice::from_ref(stream), opts).unwrap();
3019 mux.write_header().unwrap();
3020 let pkt = Packet::new(0, stream.time_base, payload.to_vec());
3021 mux.write_packet(&pkt).unwrap();
3022 mux.write_trailer().unwrap();
3023 }
3024 let bytes = std::fs::read(&tmp).unwrap();
3025 let _ = std::fs::remove_file(&tmp);
3026 bytes
3027 }
3028
3029 fn open_demux_from_bytes(bytes: Vec<u8>) -> Box<dyn Demuxer> {
3030 use std::io::Cursor;
3031 let rs: Box<dyn ReadSeek> = Box::new(Cursor::new(bytes));
3032 open_demuxer(rs, &oxideav_core::NullCodecResolver).unwrap()
3033 }
3034
3035 #[test]
3036 fn round_trip_s16_mono() {
3037 let samples: Vec<i16> = (0..1000).map(|i| ((i * 32) - 16000) as i16).collect();
3039 let mut payload = Vec::with_capacity(samples.len() * 2);
3040 for s in &samples {
3041 payload.extend_from_slice(&s.to_le_bytes());
3042 }
3043
3044 let stream = make_stream(SampleFormat::S16, 1, 48_000);
3046 let tmp = std::env::temp_dir().join("oxideav-basic-wav-test.wav");
3047 {
3048 let f = std::fs::File::create(&tmp).unwrap();
3049 let ws: Box<dyn WriteSeek> = Box::new(f);
3050 let mut mux = open_muxer(ws, std::slice::from_ref(&stream)).unwrap();
3051 mux.write_header().unwrap();
3052 let pkt = Packet::new(0, stream.time_base, payload.clone());
3053 mux.write_packet(&pkt).unwrap();
3054 mux.write_trailer().unwrap();
3055 }
3056 let rs: Box<dyn ReadSeek> = Box::new(std::fs::File::open(&tmp).unwrap());
3057 let mut dmx = open_demuxer(rs, &oxideav_core::NullCodecResolver).unwrap();
3058 assert_eq!(dmx.format_name(), "wav");
3059 assert_eq!(dmx.streams().len(), 1);
3060 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
3061 let mut out_bytes = Vec::new();
3062 loop {
3063 match dmx.next_packet() {
3064 Ok(p) => out_bytes.extend_from_slice(&p.data),
3065 Err(Error::Eof) => break,
3066 Err(e) => panic!("demux error: {e}"),
3067 }
3068 }
3069 assert_eq!(out_bytes, payload);
3070 }
3071
3072 #[test]
3075 fn round_trip_alaw_mono() {
3076 let payload: Vec<u8> = (0..=255u8).collect();
3079 let stream = make_g711_stream("pcm_alaw", 1, 8_000);
3080 let bytes = mux_to_bytes(&stream, &payload, WavMuxOptions::default(), "alaw-mono");
3081 let mut dmx = open_demux_from_bytes(bytes);
3082 assert_eq!(dmx.streams().len(), 1);
3083 let s = &dmx.streams()[0];
3084 assert_eq!(s.params.codec_id, CodecId::new("pcm_alaw"));
3085 assert_eq!(wave_format_tag(&s.params), Some(FMT_ALAW));
3086 assert_eq!(s.params.sample_format, Some(SampleFormat::S16));
3087 assert_eq!(s.params.bit_rate, Some(8 * 8_000));
3090 let mut out = Vec::new();
3091 loop {
3092 match dmx.next_packet() {
3093 Ok(p) => out.extend_from_slice(&p.data),
3094 Err(Error::Eof) => break,
3095 Err(e) => panic!("demux error: {e}"),
3096 }
3097 }
3098 assert_eq!(out, payload);
3099 }
3100
3101 #[test]
3103 fn round_trip_mulaw_stereo() {
3104 let payload: Vec<u8> = (0..512u32).map(|i| (i & 0xFF) as u8).collect();
3105 let stream = make_g711_stream("pcm_mulaw", 2, 8_000);
3106 let bytes = mux_to_bytes(&stream, &payload, WavMuxOptions::default(), "mulaw-stereo");
3107 let mut dmx = open_demux_from_bytes(bytes);
3108 let s = &dmx.streams()[0];
3109 assert_eq!(s.params.codec_id, CodecId::new("pcm_mulaw"));
3110 assert_eq!(wave_format_tag(&s.params), Some(FMT_MULAW));
3111 assert_eq!(s.params.bit_rate, Some(16 * 8_000));
3114 let mut out = Vec::new();
3115 loop {
3116 match dmx.next_packet() {
3117 Ok(p) => out.extend_from_slice(&p.data),
3118 Err(Error::Eof) => break,
3119 Err(e) => panic!("demux error: {e}"),
3120 }
3121 }
3122 assert_eq!(out, payload);
3123 }
3124
3125 #[test]
3130 fn round_trip_extensible_5_1_pcm() {
3131 const MASK_5_1: u32 = 0x0003F;
3137 let frame: [i16; 6] = [-100, 200, -300, 400, -500, 600];
3138 let mut payload = Vec::new();
3139 for _ in 0..32 {
3140 for s in &frame {
3141 payload.extend_from_slice(&s.to_le_bytes());
3142 }
3143 }
3144
3145 let mut stream = make_stream(SampleFormat::S16, 6, 48_000);
3146 stream.params.codec_id = CodecId::new("pcm_s16le");
3150 let opts = WavMuxOptions::default().with_extensible(MASK_5_1);
3151 let buf = mux_to_bytes(&stream, &payload, opts, "ext-5-1-pcm");
3152
3153 assert_eq!(&buf[12..16], b"fmt ");
3157 let fmt_size = u32::from_le_bytes([buf[16], buf[17], buf[18], buf[19]]);
3158 assert_eq!(fmt_size, 40, "EXTENSIBLE fmt chunk must be 40 bytes");
3159 assert_eq!(u16::from_le_bytes([buf[20], buf[21]]), FMT_EXTENSIBLE);
3161 assert_eq!(u16::from_le_bytes([buf[36], buf[37]]), 22);
3163
3164 let dmx = open_demux_from_bytes(buf);
3166 let s = &dmx.streams()[0];
3170 assert_eq!(s.params.codec_id, CodecId::new("pcm_s16le"));
3171 assert_eq!(wave_format_tag(&s.params), Some(FMT_EXTENSIBLE));
3172
3173 let md: std::collections::HashMap<String, String> =
3175 dmx.metadata().iter().cloned().collect();
3176 assert_eq!(
3177 md.get("wav:fmt.channel_mask"),
3178 Some(&format!("0x{MASK_5_1:08X}"))
3179 );
3180 assert_eq!(
3181 md.get("wav:fmt.valid_bits_per_sample"),
3182 Some(&"16".to_string())
3183 );
3184 assert_eq!(
3185 md.get("wav:fmt.subformat"),
3186 Some(&"00000001-0000-0010-8000-00AA00389B71".to_string())
3187 );
3188 assert_eq!(
3192 md.get("wav:fmt.channel_layout"),
3193 Some(
3194 &"FRONT_LEFT+FRONT_RIGHT+FRONT_CENTER+LOW_FREQUENCY+BACK_LEFT+BACK_RIGHT"
3195 .to_string()
3196 )
3197 );
3198 }
3199
3200 #[test]
3203 fn extensible_accessors_match_metadata() {
3204 const MASK_STEREO: u32 = 0x00003;
3205 let payload = vec![0u8; 4 * 100]; let mut stream = make_stream(SampleFormat::S16, 2, 44_100);
3208 stream.params.codec_id = CodecId::new("pcm_s16le");
3209
3210 let opts = WavMuxOptions::default().with_extensible(MASK_STEREO);
3211 let bytes = mux_to_bytes(&stream, &payload, opts, "ext-stereo-md");
3212 let dmx = open_demux_from_bytes(bytes.clone());
3213 let md: std::collections::HashMap<String, String> =
3214 dmx.metadata().iter().cloned().collect();
3215 assert_eq!(
3216 md.get("wav:fmt.channel_mask"),
3217 Some(&format!("0x{MASK_STEREO:08X}"))
3218 );
3219 assert_eq!(
3220 md.get("wav:fmt.valid_bits_per_sample"),
3221 Some(&"16".to_string())
3222 );
3223 assert_eq!(
3227 md.get("wav:fmt.channel_layout"),
3228 Some(&"FRONT_LEFT+FRONT_RIGHT".to_string())
3229 );
3230 let typed = open_wav_demuxer(Box::new(std::io::Cursor::new(bytes))).unwrap();
3231 assert_eq!(
3232 typed.channel_layout(),
3233 Some("FRONT_LEFT+FRONT_RIGHT".to_string())
3234 );
3235 assert_eq!(typed.channel_mask(), Some(MASK_STEREO));
3236 }
3237
3238 #[test]
3243 fn channel_mask_layout_decoding() {
3244 assert_eq!(channel_mask_layout(0), None);
3246
3247 assert_eq!(channel_mask_layout(0x1), Some("FRONT_LEFT".to_string()));
3249 assert_eq!(channel_mask_layout(0x8), Some("LOW_FREQUENCY".to_string()));
3250 assert_eq!(
3251 channel_mask_layout(0x20000),
3252 Some("TOP_BACK_RIGHT".to_string())
3253 );
3254
3255 assert_eq!(channel_mask_layout(0x4), Some("FRONT_CENTER".to_string()));
3257
3258 assert_eq!(
3261 channel_mask_layout(0x33),
3262 Some("FRONT_LEFT+FRONT_RIGHT+BACK_LEFT+BACK_RIGHT".to_string())
3263 );
3264
3265 assert_eq!(
3267 channel_mask_layout(0x63F),
3268 Some(
3269 "FRONT_LEFT+FRONT_RIGHT+FRONT_CENTER+LOW_FREQUENCY\
3270 +BACK_LEFT+BACK_RIGHT+SIDE_LEFT+SIDE_RIGHT"
3271 .to_string()
3272 )
3273 );
3274
3275 assert_eq!(
3278 channel_mask_layout(0x40001),
3279 Some("FRONT_LEFT+UNKNOWN(0x40000)".to_string())
3280 );
3281 assert_eq!(
3282 channel_mask_layout(0x80000000),
3283 Some("UNKNOWN(0x80000000)".to_string())
3284 );
3285 }
3286
3287 #[test]
3291 fn extensible_alaw_through_guid() {
3292 let payload: Vec<u8> = (0..=255u8).collect();
3293 let stream = make_g711_stream("pcm_alaw", 1, 8_000);
3294
3295 let opts = WavMuxOptions::default().with_extensible(0x00004); let bytes = mux_to_bytes(&stream, &payload, opts, "ext-alaw");
3297 let mut dmx = open_demux_from_bytes(bytes);
3298 let s = &dmx.streams()[0];
3299 assert_eq!(s.params.codec_id, CodecId::new("pcm_alaw"));
3304 assert_eq!(wave_format_tag(&s.params), Some(FMT_EXTENSIBLE));
3305 let mut out = Vec::new();
3306 loop {
3307 match dmx.next_packet() {
3308 Ok(p) => out.extend_from_slice(&p.data),
3309 Err(Error::Eof) => break,
3310 Err(e) => panic!("demux error: {e}"),
3311 }
3312 }
3313 assert_eq!(out, payload);
3314 }
3315
3316 #[test]
3321 fn extensible_unknown_guid_synthesised_id() {
3322 let bogus_guid: [u8; 16] = [
3326 0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC,
3327 0xDE, 0xF0,
3328 ];
3329 let mut buf = Vec::new();
3330 buf.extend_from_slice(b"RIFF");
3331 buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(b"WAVE");
3333 buf.extend_from_slice(b"fmt ");
3334 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");
3346 buf.extend_from_slice(&0u32.to_le_bytes()); use std::io::Cursor;
3349 let rs: Box<dyn ReadSeek> = Box::new(Cursor::new(buf));
3350 let dmx_res = open_demuxer(rs, &oxideav_core::NullCodecResolver);
3351 let dmx = dmx_res.expect("unknown-GUID extensible stream still parses");
3352 let id = dmx.streams()[0].params.codec_id.as_str().to_string();
3353 assert!(
3354 id.starts_with("wav:guid_"),
3355 "unknown GUID must synthesise wav:guid_<text>, got {id:?}"
3356 );
3357 assert!(
3360 id.contains("EFBEADDE-FECA-BEBA"),
3361 "synthesised id must carry the canonical GUID text, got {id:?}"
3362 );
3363 }
3364
3365 fn extensible_wav_with_guid(guid: &[u8; 16], bits: u16) -> Vec<u8> {
3370 let block_align = bits / 8;
3371 let mut buf = Vec::new();
3372 buf.extend_from_slice(b"RIFF");
3373 buf.extend_from_slice(&0u32.to_le_bytes());
3374 buf.extend_from_slice(b"WAVE");
3375 buf.extend_from_slice(b"fmt ");
3376 buf.extend_from_slice(&40u32.to_le_bytes());
3377 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");
3388 buf.extend_from_slice(&0u32.to_le_bytes());
3389 buf
3390 }
3391
3392 #[test]
3398 fn waveformatex_tag_extracts_embedded_format_tag() {
3399 assert_eq!(waveformatex_tag(&GUID_PCM), Some(0x0001));
3400 assert_eq!(waveformatex_tag(&GUID_IEEE_FLOAT), Some(0x0003));
3401 assert_eq!(waveformatex_tag(&GUID_ALAW), Some(0x0006));
3402 assert_eq!(waveformatex_tag(&GUID_MULAW), Some(0x0007));
3403 let mut mp3_guid = GUID_PCM;
3406 mp3_guid[0] = 0x55;
3407 mp3_guid[1] = 0x00;
3408 assert_eq!(waveformatex_tag(&mp3_guid), Some(0x0055));
3409 let bogus: [u8; 16] = [
3412 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38,
3413 0x9B, 0x72, ];
3415 assert_eq!(waveformatex_tag(&bogus), None);
3416 }
3417
3418 #[test]
3423 fn extensible_template_guid_dispatches_through_legacy_tag() {
3424 let buf = extensible_wav_with_guid(&GUID_IEEE_FLOAT, 32);
3425 use std::io::Cursor;
3426 let rs: Box<dyn ReadSeek> = Box::new(Cursor::new(buf));
3427 let dmx = open_demuxer(rs, &oxideav_core::NullCodecResolver)
3428 .expect("IEEE-float template GUID resolves");
3429 let s = &dmx.streams()[0];
3430 assert_eq!(s.params.codec_id.as_str(), "pcm_f32le");
3431 let md: std::collections::HashMap<_, _> = dmx.metadata().iter().cloned().collect();
3432 assert_eq!(
3433 md.get("wav:fmt.subformat_tag").map(String::as_str),
3434 Some("0x0003")
3435 );
3436 }
3437
3438 #[test]
3444 fn extensible_template_guid_unmapped_tag_surfaces_tag() {
3445 let mut mp3_guid = GUID_PCM;
3446 mp3_guid[0] = 0x55; let buf = extensible_wav_with_guid(&mp3_guid, 16);
3448 use std::io::Cursor;
3449 let rs: Box<dyn ReadSeek> = Box::new(Cursor::new(buf));
3450 let dmx = open_demuxer(rs, &oxideav_core::NullCodecResolver)
3451 .expect("MP3 template GUID still parses");
3452 let s = &dmx.streams()[0];
3453 assert!(
3454 s.params.codec_id.as_str().starts_with("wav:guid_"),
3455 "unmapped tag must synthesise wav:guid_<text>, got {:?}",
3456 s.params.codec_id.as_str()
3457 );
3458 let md: std::collections::HashMap<_, _> = dmx.metadata().iter().cloned().collect();
3459 assert_eq!(
3460 md.get("wav:fmt.subformat_tag").map(String::as_str),
3461 Some("0x0055")
3462 );
3463 }
3464
3465 #[test]
3471 fn extensible_short_cbsize_rejected() {
3472 let mut buf = Vec::new();
3473 buf.extend_from_slice(b"RIFF");
3474 buf.extend_from_slice(&0u32.to_le_bytes());
3475 buf.extend_from_slice(b"WAVE");
3476 buf.extend_from_slice(b"fmt ");
3477 buf.extend_from_slice(&40u32.to_le_bytes());
3479 buf.extend_from_slice(&FMT_EXTENSIBLE.to_le_bytes());
3480 buf.extend_from_slice(&1u16.to_le_bytes());
3481 buf.extend_from_slice(&44_100u32.to_le_bytes());
3482 buf.extend_from_slice(&88_200u32.to_le_bytes());
3483 buf.extend_from_slice(&2u16.to_le_bytes());
3484 buf.extend_from_slice(&16u16.to_le_bytes());
3485 buf.extend_from_slice(&10u16.to_le_bytes()); buf.extend_from_slice(&[0u8; 20]); buf.extend_from_slice(b"data");
3488 buf.extend_from_slice(&0u32.to_le_bytes());
3489
3490 use std::io::Cursor;
3491 let rs: Box<dyn ReadSeek> = Box::new(Cursor::new(buf));
3492 let err = open_demuxer(rs, &oxideav_core::NullCodecResolver).err();
3493 assert!(
3494 matches!(err, Some(Error::InvalidData(_))),
3495 "short cbSize must yield Error::InvalidData, got {err:?}"
3496 );
3497 }
3498
3499 fn wav_with_bext(bext_body: &[u8]) -> Vec<u8> {
3504 let mut buf = Vec::new();
3505 buf.extend_from_slice(b"RIFF");
3506 buf.extend_from_slice(&0u32.to_le_bytes());
3507 buf.extend_from_slice(b"WAVE");
3508 buf.extend_from_slice(b"fmt ");
3510 buf.extend_from_slice(&16u32.to_le_bytes());
3511 buf.extend_from_slice(&FMT_PCM.to_le_bytes());
3512 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");
3519 buf.extend_from_slice(&(bext_body.len() as u32).to_le_bytes());
3520 buf.extend_from_slice(bext_body);
3521 if bext_body.len() % 2 == 1 {
3522 buf.push(0); }
3524 buf.extend_from_slice(b"data");
3526 buf.extend_from_slice(&0u32.to_le_bytes());
3527 buf
3528 }
3529
3530 #[allow(clippy::too_many_arguments)]
3533 fn make_bext_v2(
3534 description: &str,
3535 originator: &str,
3536 originator_ref: &str,
3537 date: &str,
3538 time: &str,
3539 time_reference: u64,
3540 umid: &[u8; 64],
3541 loudness: [i16; 5],
3542 coding_history: &str,
3543 ) -> Vec<u8> {
3544 fn put_ascii(buf: &mut Vec<u8>, s: &str, width: usize) {
3545 let bytes = s.as_bytes();
3546 let n = bytes.len().min(width);
3547 buf.extend_from_slice(&bytes[..n]);
3548 buf.resize(buf.len() + (width - n), 0);
3549 }
3550 let mut b = Vec::new();
3551 put_ascii(&mut b, description, 256);
3552 put_ascii(&mut b, originator, 32);
3553 put_ascii(&mut b, originator_ref, 32);
3554 put_ascii(&mut b, date, 10);
3555 put_ascii(&mut b, time, 8);
3556 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);
3560 for v in &loudness {
3561 b.extend_from_slice(&v.to_le_bytes());
3562 }
3563 b.resize(BEXT_FIXED_LEN, 0); assert_eq!(b.len(), BEXT_FIXED_LEN);
3565 b.extend_from_slice(coding_history.as_bytes());
3566 b
3567 }
3568
3569 #[test]
3575 fn bext_loudness_formatting() {
3576 assert_eq!(fmt_loudness(-2264), "-22.64");
3577 assert_eq!(fmt_loudness(-2265), "-22.65");
3578 assert_eq!(fmt_loudness(0), "0.00");
3579 assert_eq!(fmt_loudness(2300), "23.00");
3580 assert_eq!(fmt_loudness(-50), "-0.50"); assert_eq!(fmt_loudness(7), "0.07");
3582 assert_eq!(fmt_loudness(-1), "-0.01");
3583 }
3584
3585 #[test]
3590 fn bext_v2_full_metadata() {
3591 let mut umid = [0u8; 64];
3592 umid[0] = 0x06;
3593 umid[1] = 0x0a;
3594 umid[63] = 0xff;
3595 let body = make_bext_v2(
3596 "Scene 1 take 3",
3597 "OxideAV Recorder",
3598 "USABC2400001",
3599 "2026-05-23",
3600 "14:30:00",
3601 0x0000_0001_2345_6789,
3603 &umid,
3604 [-2305, 700, -120, -1850, -2010],
3605 "A=PCM,F=48000,W=24,M=stereo,T=OxideAV\r\n",
3606 );
3607 let bytes = wav_with_bext(&body);
3608 let dmx = open_demux_from_bytes(bytes);
3609 let md: std::collections::HashMap<String, String> =
3610 dmx.metadata().iter().cloned().collect();
3611
3612 assert_eq!(
3613 md.get("wav:bext.description"),
3614 Some(&"Scene 1 take 3".to_string())
3615 );
3616 assert_eq!(
3617 md.get("wav:bext.originator"),
3618 Some(&"OxideAV Recorder".to_string())
3619 );
3620 assert_eq!(
3621 md.get("wav:bext.originator_reference"),
3622 Some(&"USABC2400001".to_string())
3623 );
3624 assert_eq!(
3625 md.get("wav:bext.origination_date"),
3626 Some(&"2026-05-23".to_string())
3627 );
3628 assert_eq!(
3629 md.get("wav:bext.origination_time"),
3630 Some(&"14:30:00".to_string())
3631 );
3632 assert_eq!(
3633 md.get("wav:bext.time_reference"),
3634 Some(&0x0000_0001_2345_6789u64.to_string())
3635 );
3636 assert_eq!(md.get("wav:bext.version"), Some(&"2".to_string()));
3637 assert_eq!(
3638 md.get("wav:bext.loudness_value"),
3639 Some(&"-23.05".to_string())
3640 );
3641 assert_eq!(md.get("wav:bext.loudness_range"), Some(&"7.00".to_string()));
3642 assert_eq!(
3643 md.get("wav:bext.max_true_peak_level"),
3644 Some(&"-1.20".to_string())
3645 );
3646 assert_eq!(
3647 md.get("wav:bext.max_momentary_loudness"),
3648 Some(&"-18.50".to_string())
3649 );
3650 assert_eq!(
3651 md.get("wav:bext.max_short_term_loudness"),
3652 Some(&"-20.10".to_string())
3653 );
3654 let umid_hex = md.get("wav:bext.umid").expect("umid present");
3656 assert!(umid_hex.starts_with("060a"), "umid hex {umid_hex:?}");
3657 assert!(umid_hex.ends_with("ff"), "umid hex {umid_hex:?}");
3658 assert_eq!(umid_hex.len(), 128); assert_eq!(
3660 md.get("wav:bext.coding_history"),
3661 Some(&"A=PCM,F=48000,W=24,M=stereo,T=OxideAV".to_string())
3662 );
3663 }
3664
3665 #[test]
3669 fn bext_v0_omits_umid_and_loudness() {
3670 let mut body = vec![0u8; BEXT_FIXED_LEN];
3672 let desc = b"Field recording";
3674 body[..desc.len()].copy_from_slice(desc);
3675 body[338..342].copy_from_slice(&12_345u32.to_le_bytes());
3677 let bytes = wav_with_bext(&body);
3679 let dmx = open_demux_from_bytes(bytes);
3680 let md: std::collections::HashMap<String, String> =
3681 dmx.metadata().iter().cloned().collect();
3682
3683 assert_eq!(
3684 md.get("wav:bext.description"),
3685 Some(&"Field recording".to_string())
3686 );
3687 assert_eq!(md.get("wav:bext.version"), Some(&"0".to_string()));
3688 assert_eq!(
3689 md.get("wav:bext.time_reference"),
3690 Some(&"12345".to_string())
3691 );
3692 assert!(!md.contains_key("wav:bext.umid"));
3694 assert!(!md.contains_key("wav:bext.loudness_value"));
3695 assert!(!md.contains_key("wav:bext.max_true_peak_level"));
3696 }
3697
3698 #[test]
3702 fn bext_truncated_is_skipped() {
3703 let body = vec![0u8; 100]; let bytes = wav_with_bext(&body);
3705 let dmx = open_demux_from_bytes(bytes);
3706 let md: std::collections::HashMap<String, String> =
3707 dmx.metadata().iter().cloned().collect();
3708 assert!(md.keys().all(|k| !k.starts_with("wav:bext.")));
3709 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
3711 }
3712
3713 fn wav_with_cue_and_adtl(cue_body: &[u8], adtl_body: Option<&[u8]>) -> Vec<u8> {
3717 let mut buf = Vec::new();
3718 buf.extend_from_slice(b"RIFF");
3719 buf.extend_from_slice(&0u32.to_le_bytes());
3720 buf.extend_from_slice(b"WAVE");
3721 buf.extend_from_slice(b"fmt ");
3723 buf.extend_from_slice(&16u32.to_le_bytes());
3724 buf.extend_from_slice(&FMT_PCM.to_le_bytes());
3725 buf.extend_from_slice(&1u16.to_le_bytes());
3726 buf.extend_from_slice(&8_000u32.to_le_bytes());
3727 buf.extend_from_slice(&16_000u32.to_le_bytes());
3728 buf.extend_from_slice(&2u16.to_le_bytes());
3729 buf.extend_from_slice(&16u16.to_le_bytes());
3730 buf.extend_from_slice(b"cue ");
3732 buf.extend_from_slice(&(cue_body.len() as u32).to_le_bytes());
3733 buf.extend_from_slice(cue_body);
3734 if cue_body.len() % 2 == 1 {
3735 buf.push(0);
3736 }
3737 if let Some(adtl) = adtl_body {
3739 buf.extend_from_slice(b"LIST");
3741 buf.extend_from_slice(&((adtl.len() + 4) as u32).to_le_bytes());
3742 buf.extend_from_slice(b"adtl");
3743 buf.extend_from_slice(adtl);
3744 if (adtl.len() + 4) % 2 == 1 {
3745 buf.push(0);
3746 }
3747 }
3748 buf.extend_from_slice(b"data");
3750 buf.extend_from_slice(&0u32.to_le_bytes());
3751 buf
3752 }
3753
3754 fn cue_point(
3757 dw_name: u32,
3758 dw_position: u32,
3759 fcc_chunk: &[u8; 4],
3760 dw_chunk_start: u32,
3761 dw_block_start: u32,
3762 dw_sample_offset: u32,
3763 ) -> Vec<u8> {
3764 let mut b = Vec::with_capacity(24);
3765 b.extend_from_slice(&dw_name.to_le_bytes());
3766 b.extend_from_slice(&dw_position.to_le_bytes());
3767 b.extend_from_slice(fcc_chunk);
3768 b.extend_from_slice(&dw_chunk_start.to_le_bytes());
3769 b.extend_from_slice(&dw_block_start.to_le_bytes());
3770 b.extend_from_slice(&dw_sample_offset.to_le_bytes());
3771 b
3772 }
3773
3774 fn adtl_text_subchunk(id: &[u8; 4], dw_name: u32, text: &str) -> Vec<u8> {
3777 let mut b = Vec::new();
3778 b.extend_from_slice(id);
3779 let body_len = 4 + text.len() + 1;
3781 b.extend_from_slice(&(body_len as u32).to_le_bytes());
3782 b.extend_from_slice(&dw_name.to_le_bytes());
3783 b.extend_from_slice(text.as_bytes());
3784 b.push(0);
3785 if body_len % 2 == 1 {
3786 b.push(0);
3787 }
3788 b
3789 }
3790
3791 #[test]
3795 fn cue_and_adtl_full_metadata() {
3796 let mut cue_body = Vec::new();
3798 cue_body.extend_from_slice(&2u32.to_le_bytes()); cue_body.extend(cue_point(1, 0, b"data", 0, 0, 0));
3800 cue_body.extend(cue_point(2, 12_345, b"data", 0, 0, 12_345));
3801
3802 let mut adtl = Vec::new();
3804 adtl.extend(adtl_text_subchunk(b"labl", 1, "Intro"));
3805 adtl.extend(adtl_text_subchunk(b"note", 1, "Fade-in"));
3806 adtl.extend(adtl_text_subchunk(b"labl", 2, "Verse"));
3807 adtl.extend(adtl_text_subchunk(b"note", 2, "Vocal entry"));
3808
3809 let bytes = wav_with_cue_and_adtl(&cue_body, Some(&adtl));
3810 let dmx = open_demux_from_bytes(bytes);
3811 let md: std::collections::HashMap<String, String> =
3812 dmx.metadata().iter().cloned().collect();
3813
3814 assert_eq!(md.get("wav:cue.count"), Some(&"2".to_string()));
3815 assert_eq!(md.get("wav:cue.1.position"), Some(&"0".to_string()));
3816 assert_eq!(md.get("wav:cue.1.fcc_chunk"), Some(&"data".to_string()));
3817 assert_eq!(md.get("wav:cue.1.chunk_start"), Some(&"0".to_string()));
3818 assert_eq!(md.get("wav:cue.1.block_start"), Some(&"0".to_string()));
3819 assert_eq!(md.get("wav:cue.1.sample_offset"), Some(&"0".to_string()));
3820 assert_eq!(md.get("wav:cue.2.position"), Some(&"12345".to_string()));
3821 assert_eq!(
3822 md.get("wav:cue.2.sample_offset"),
3823 Some(&"12345".to_string())
3824 );
3825
3826 assert_eq!(md.get("wav:adtl.labl.1"), Some(&"Intro".to_string()));
3827 assert_eq!(md.get("wav:adtl.note.1"), Some(&"Fade-in".to_string()));
3828 assert_eq!(md.get("wav:adtl.labl.2"), Some(&"Verse".to_string()));
3829 assert_eq!(md.get("wav:adtl.note.2"), Some(&"Vocal entry".to_string()));
3830 }
3831
3832 #[test]
3835 fn adtl_ltxt_segment_metadata() {
3836 let mut cue_body = Vec::new();
3838 cue_body.extend_from_slice(&1u32.to_le_bytes());
3839 cue_body.extend(cue_point(7, 1000, b"data", 0, 0, 1000));
3840
3841 let mut ltxt_body = Vec::new();
3843 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");
3851 ltxt_body.push(0);
3852
3853 let mut adtl = Vec::new();
3854 adtl.extend_from_slice(b"ltxt");
3855 adtl.extend_from_slice(&(ltxt_body.len() as u32).to_le_bytes());
3856 adtl.extend_from_slice(<xt_body);
3857 if ltxt_body.len() % 2 == 1 {
3858 adtl.push(0);
3859 }
3860
3861 let bytes = wav_with_cue_and_adtl(&cue_body, Some(&adtl));
3862 let dmx = open_demux_from_bytes(bytes);
3863 let md: std::collections::HashMap<String, String> =
3864 dmx.metadata().iter().cloned().collect();
3865
3866 assert_eq!(md.get("wav:adtl.ltxt.7.length"), Some(&"4410".to_string()));
3867 assert_eq!(md.get("wav:adtl.ltxt.7.purpose"), Some(&"scrp".to_string()));
3868 assert_eq!(
3869 md.get("wav:adtl.ltxt.7.text"),
3870 Some(&"Hello world".to_string())
3871 );
3872 assert_eq!(md.get("wav:adtl.ltxt.7.country"), Some(&"44".to_string()));
3875 assert_eq!(
3876 md.get("wav:adtl.ltxt.7.country_name"),
3877 Some(&"United Kingdom".to_string())
3878 );
3879 assert_eq!(md.get("wav:adtl.ltxt.7.language"), Some(&"9".to_string()));
3880 assert_eq!(md.get("wav:adtl.ltxt.7.dialect"), Some(&"2".to_string()));
3881 assert_eq!(
3882 md.get("wav:adtl.ltxt.7.language_name"),
3883 Some(&"UK English".to_string())
3884 );
3885 assert_eq!(
3886 md.get("wav:adtl.ltxt.7.code_page"),
3887 Some(&"1252".to_string())
3888 );
3889 }
3890
3891 #[test]
3895 fn adtl_ltxt_zero_locale_fields_surface() {
3896 let mut cue_body = Vec::new();
3897 cue_body.extend_from_slice(&1u32.to_le_bytes());
3898 cue_body.extend(cue_point(3, 0, b"data", 0, 0, 0));
3899
3900 let mut ltxt_body = Vec::new();
3901 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();
3907 adtl.extend_from_slice(b"ltxt");
3908 adtl.extend_from_slice(&(ltxt_body.len() as u32).to_le_bytes());
3909 adtl.extend_from_slice(<xt_body);
3910
3911 let bytes = wav_with_cue_and_adtl(&cue_body, Some(&adtl));
3912 let dmx = open_demux_from_bytes(bytes);
3913 let md: std::collections::HashMap<String, String> =
3914 dmx.metadata().iter().cloned().collect();
3915
3916 assert_eq!(md.get("wav:adtl.ltxt.3.country"), Some(&"0".to_string()));
3917 assert_eq!(
3918 md.get("wav:adtl.ltxt.3.country_name"),
3919 Some(&"None".to_string())
3920 );
3921 assert_eq!(md.get("wav:adtl.ltxt.3.language"), Some(&"0".to_string()));
3922 assert_eq!(md.get("wav:adtl.ltxt.3.dialect"), Some(&"0".to_string()));
3923 assert_eq!(
3924 md.get("wav:adtl.ltxt.3.language_name"),
3925 Some(&"None".to_string())
3926 );
3927 assert_eq!(md.get("wav:adtl.ltxt.3.code_page"), Some(&"0".to_string()));
3928 assert_eq!(md.get("wav:adtl.ltxt.3.text"), None);
3930 }
3931
3932 #[test]
3936 fn adtl_file_subchunk_metadata() {
3937 let mut cue_body = Vec::new();
3938 cue_body.extend_from_slice(&1u32.to_le_bytes());
3939 cue_body.extend(cue_point(5, 0, b"data", 0, 0, 0));
3940
3941 let mut file_body = Vec::new();
3944 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();
3949 adtl.extend_from_slice(b"file");
3950 adtl.extend_from_slice(&(file_body.len() as u32).to_le_bytes());
3951 adtl.extend_from_slice(&file_body);
3952 if file_body.len() % 2 == 1 {
3953 adtl.push(0);
3954 }
3955 let mut file2_body = Vec::new();
3958 file2_body.extend_from_slice(&6u32.to_le_bytes()); file2_body.extend_from_slice(&0u32.to_le_bytes()); adtl.extend_from_slice(b"file");
3961 adtl.extend_from_slice(&(file2_body.len() as u32).to_le_bytes());
3962 adtl.extend_from_slice(&file2_body);
3963
3964 let bytes = wav_with_cue_and_adtl(&cue_body, Some(&adtl));
3965 let dmx = open_demux_from_bytes(bytes);
3966 let md: std::collections::HashMap<String, String> =
3967 dmx.metadata().iter().cloned().collect();
3968
3969 assert_eq!(
3970 md.get("wav:adtl.file.5.med_type"),
3971 Some(&"RDIB".to_string())
3972 );
3973 assert_eq!(md.get("wav:adtl.file.5.body_len"), Some(&"11".to_string()));
3974 assert_eq!(md.get("wav:adtl.file.6.med_type"), Some(&"0".to_string()));
3976 assert_eq!(md.get("wav:adtl.file.6.body_len"), Some(&"0".to_string()));
3977 }
3978
3979 #[test]
3982 fn adtl_file_truncated_is_skipped() {
3983 let cue_body = 0u32.to_le_bytes().to_vec();
3984 let mut adtl = Vec::new();
3985 adtl.extend_from_slice(b"file");
3986 adtl.extend_from_slice(&6u32.to_le_bytes()); adtl.extend_from_slice(&[0u8; 6]);
3988 let bytes = wav_with_cue_and_adtl(&cue_body, Some(&adtl));
3989 let dmx = open_demux_from_bytes(bytes);
3990 assert!(dmx
3991 .metadata()
3992 .iter()
3993 .all(|(k, _)| !k.starts_with("wav:adtl.file.")));
3994 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
3995 }
3996
3997 #[test]
4001 fn cue_truncated_count_is_clamped() {
4002 let mut cue_body = Vec::new();
4004 cue_body.extend_from_slice(&5u32.to_le_bytes());
4005 cue_body.extend(cue_point(42, 100, b"data", 0, 0, 100));
4006 let bytes = wav_with_cue_and_adtl(&cue_body, None);
4007 let dmx = open_demux_from_bytes(bytes);
4008 let md: std::collections::HashMap<String, String> =
4009 dmx.metadata().iter().cloned().collect();
4010 assert_eq!(md.get("wav:cue.count"), Some(&"1".to_string()));
4011 assert_eq!(md.get("wav:cue.42.position"), Some(&"100".to_string()));
4012 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
4014 }
4015
4016 #[test]
4021 fn adtl_without_cue_still_surfaces() {
4022 let mut adtl = Vec::new();
4023 adtl.extend(adtl_text_subchunk(b"labl", 99, "Orphan label"));
4024 let cue_body = 0u32.to_le_bytes().to_vec();
4026 let bytes = wav_with_cue_and_adtl(&cue_body, Some(&adtl));
4027 let dmx = open_demux_from_bytes(bytes);
4028 let md: std::collections::HashMap<String, String> =
4029 dmx.metadata().iter().cloned().collect();
4030 assert_eq!(md.get("wav:cue.count"), Some(&"0".to_string()));
4031 assert_eq!(
4032 md.get("wav:adtl.labl.99"),
4033 Some(&"Orphan label".to_string())
4034 );
4035 }
4036
4037 fn wav_with_smpl_and_inst(smpl_body: Option<&[u8]>, inst_body: Option<&[u8]>) -> Vec<u8> {
4042 let mut buf = Vec::new();
4043 buf.extend_from_slice(b"RIFF");
4044 buf.extend_from_slice(&0u32.to_le_bytes());
4045 buf.extend_from_slice(b"WAVE");
4046 buf.extend_from_slice(b"fmt ");
4048 buf.extend_from_slice(&16u32.to_le_bytes());
4049 buf.extend_from_slice(&FMT_PCM.to_le_bytes());
4050 buf.extend_from_slice(&1u16.to_le_bytes());
4051 buf.extend_from_slice(&8_000u32.to_le_bytes());
4052 buf.extend_from_slice(&16_000u32.to_le_bytes());
4053 buf.extend_from_slice(&2u16.to_le_bytes());
4054 buf.extend_from_slice(&16u16.to_le_bytes());
4055 if let Some(smpl) = smpl_body {
4056 buf.extend_from_slice(b"smpl");
4057 buf.extend_from_slice(&(smpl.len() as u32).to_le_bytes());
4058 buf.extend_from_slice(smpl);
4059 if smpl.len() % 2 == 1 {
4060 buf.push(0);
4061 }
4062 }
4063 if let Some(inst) = inst_body {
4064 buf.extend_from_slice(b"inst");
4065 buf.extend_from_slice(&(inst.len() as u32).to_le_bytes());
4066 buf.extend_from_slice(inst);
4067 if inst.len() % 2 == 1 {
4068 buf.push(0);
4069 }
4070 }
4071 buf.extend_from_slice(b"data");
4073 buf.extend_from_slice(&0u32.to_le_bytes());
4074 buf
4075 }
4076
4077 #[allow(clippy::too_many_arguments)]
4080 fn smpl_body(
4081 manufacturer: u32,
4082 product: u32,
4083 sample_period: u32,
4084 midi_unity_note: u32,
4085 midi_pitch_fraction: u32,
4086 smpte_format: u32,
4087 smpte_offset: u32,
4088 c_sample_loops_claimed: u32,
4089 cb_sampler_data: u32,
4090 loops: &[(u32, u32, u32, u32, u32, u32)],
4091 ) -> Vec<u8> {
4092 let mut b = Vec::new();
4093 for v in [
4094 manufacturer,
4095 product,
4096 sample_period,
4097 midi_unity_note,
4098 midi_pitch_fraction,
4099 smpte_format,
4100 smpte_offset,
4101 c_sample_loops_claimed,
4102 cb_sampler_data,
4103 ] {
4104 b.extend_from_slice(&v.to_le_bytes());
4105 }
4106 for &(id, ty, start, end, frac, count) in loops {
4107 for v in [id, ty, start, end, frac, count] {
4108 b.extend_from_slice(&v.to_le_bytes());
4109 }
4110 }
4111 b
4112 }
4113
4114 #[test]
4118 fn smpl_full_metadata() {
4119 let body = smpl_body(
4121 0x1234, 0xDEAD_BEEF, 22_675, 60, 0x8000_0000, 30, 0x01_02_03_04, 1, 0, &[(7, 0, 0, 1000, 0, 0)], );
4132 let bytes = wav_with_smpl_and_inst(Some(&body), None);
4133 let dmx = open_demux_from_bytes(bytes);
4134 let md: std::collections::HashMap<String, String> =
4135 dmx.metadata().iter().cloned().collect();
4136 assert_eq!(md.get("wav:smpl.manufacturer"), Some(&"4660".to_string()));
4137 assert_eq!(md.get("wav:smpl.product"), Some(&"3735928559".to_string()));
4138 assert_eq!(md.get("wav:smpl.sample_period"), Some(&"22675".to_string()));
4139 assert_eq!(md.get("wav:smpl.midi_unity_note"), Some(&"60".to_string()));
4140 assert_eq!(
4141 md.get("wav:smpl.midi_pitch_fraction"),
4142 Some(&"2147483648".to_string())
4143 );
4144 assert_eq!(md.get("wav:smpl.smpte_format"), Some(&"30".to_string()));
4145 assert_eq!(
4146 md.get("wav:smpl.smpte_offset"),
4147 Some(&"01:02:03:04".to_string())
4148 );
4149 assert_eq!(md.get("wav:smpl.sampler_data_len"), Some(&"0".to_string()));
4150 assert_eq!(md.get("wav:smpl.num_sample_loops"), Some(&"1".to_string()));
4151 assert_eq!(
4152 md.get("wav:smpl.loop.0.cue_point_id"),
4153 Some(&"7".to_string())
4154 );
4155 assert_eq!(md.get("wav:smpl.loop.0.type"), Some(&"0".to_string()));
4156 assert_eq!(md.get("wav:smpl.loop.0.start"), Some(&"0".to_string()));
4157 assert_eq!(md.get("wav:smpl.loop.0.end"), Some(&"1000".to_string()));
4158 assert_eq!(md.get("wav:smpl.loop.0.fraction"), Some(&"0".to_string()));
4159 assert_eq!(md.get("wav:smpl.loop.0.play_count"), Some(&"0".to_string()));
4160 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
4162 }
4163
4164 #[test]
4169 fn smpl_loop_count_clamped_to_body() {
4170 let body = smpl_body(
4173 0,
4174 0,
4175 0,
4176 60,
4177 0,
4178 0,
4179 0,
4180 5,
4181 0,
4182 &[(1, 0, 0, 100, 0, 0)],
4183 );
4184 let bytes = wav_with_smpl_and_inst(Some(&body), None);
4185 let dmx = open_demux_from_bytes(bytes);
4186 let md: std::collections::HashMap<String, String> =
4187 dmx.metadata().iter().cloned().collect();
4188 assert_eq!(md.get("wav:smpl.num_sample_loops"), Some(&"1".to_string()));
4189 assert!(md.contains_key("wav:smpl.loop.0.cue_point_id"));
4191 assert!(!md.contains_key("wav:smpl.loop.1.cue_point_id"));
4192 assert!(!md.contains_key("wav:smpl.loop.4.cue_point_id"));
4193 }
4194
4195 #[test]
4199 fn smpl_truncated_is_skipped() {
4200 let body = vec![0u8; 20]; let bytes = wav_with_smpl_and_inst(Some(&body), None);
4202 let dmx = open_demux_from_bytes(bytes);
4203 let md: std::collections::HashMap<String, String> =
4204 dmx.metadata().iter().cloned().collect();
4205 assert!(md.keys().all(|k| !k.starts_with("wav:smpl.")));
4206 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
4207 }
4208
4209 #[test]
4213 fn inst_full_metadata() {
4214 let body: Vec<u8> = vec![60, 0xFD, 0xFA, 36, 96, 1, 127];
4216 let bytes = wav_with_smpl_and_inst(None, Some(&body));
4217 let dmx = open_demux_from_bytes(bytes);
4218 let md: std::collections::HashMap<String, String> =
4219 dmx.metadata().iter().cloned().collect();
4220 assert_eq!(md.get("wav:inst.unshifted_note"), Some(&"60".to_string()));
4221 assert_eq!(md.get("wav:inst.fine_tune"), Some(&"-3".to_string()));
4222 assert_eq!(md.get("wav:inst.gain"), Some(&"-6".to_string()));
4223 assert_eq!(md.get("wav:inst.low_note"), Some(&"36".to_string()));
4224 assert_eq!(md.get("wav:inst.high_note"), Some(&"96".to_string()));
4225 assert_eq!(md.get("wav:inst.low_velocity"), Some(&"1".to_string()));
4226 assert_eq!(md.get("wav:inst.high_velocity"), Some(&"127".to_string()));
4227 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
4228 }
4229
4230 #[test]
4233 fn inst_truncated_is_skipped() {
4234 let body = vec![0u8; 5]; let bytes = wav_with_smpl_and_inst(None, Some(&body));
4236 let dmx = open_demux_from_bytes(bytes);
4237 let md: std::collections::HashMap<String, String> =
4238 dmx.metadata().iter().cloned().collect();
4239 assert!(md.keys().all(|k| !k.starts_with("wav:inst.")));
4240 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
4241 }
4242
4243 #[test]
4248 fn smpl_and_inst_coexist_with_padding() {
4249 let smpl = smpl_body(0, 0, 0, 64, 0, 0, 0, 0, 0, &[]);
4250 let inst: Vec<u8> = vec![64, 0, 0, 0, 127, 1, 127]; let bytes = wav_with_smpl_and_inst(Some(&smpl), Some(&inst));
4252 let dmx = open_demux_from_bytes(bytes);
4253 let md: std::collections::HashMap<String, String> =
4254 dmx.metadata().iter().cloned().collect();
4255 assert_eq!(md.get("wav:smpl.midi_unity_note"), Some(&"64".to_string()));
4256 assert_eq!(md.get("wav:inst.unshifted_note"), Some(&"64".to_string()));
4257 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
4258 }
4259
4260 fn wav_with_acid(acid_body: &[u8]) -> Vec<u8> {
4264 let mut buf = Vec::new();
4265 buf.extend_from_slice(b"RIFF");
4266 buf.extend_from_slice(&0u32.to_le_bytes());
4267 buf.extend_from_slice(b"WAVE");
4268 buf.extend_from_slice(b"fmt ");
4269 buf.extend_from_slice(&16u32.to_le_bytes());
4270 buf.extend_from_slice(&FMT_PCM.to_le_bytes());
4271 buf.extend_from_slice(&1u16.to_le_bytes());
4272 buf.extend_from_slice(&8_000u32.to_le_bytes());
4273 buf.extend_from_slice(&16_000u32.to_le_bytes());
4274 buf.extend_from_slice(&2u16.to_le_bytes());
4275 buf.extend_from_slice(&16u16.to_le_bytes());
4276 buf.extend_from_slice(b"acid");
4277 buf.extend_from_slice(&(acid_body.len() as u32).to_le_bytes());
4278 buf.extend_from_slice(acid_body);
4279 if acid_body.len() % 2 == 1 {
4280 buf.push(0);
4281 }
4282 buf.extend_from_slice(b"data");
4283 buf.extend_from_slice(&0u32.to_le_bytes());
4284 buf
4285 }
4286
4287 #[test]
4292 fn acid_chunk_byte_layout_pinned() {
4293 let acid = AcidChunk {
4294 flags: ACID_FLAG_ROOT_NOTE_SET | ACID_FLAG_STRETCH,
4295 root_note: 57, reserved: [0x80, 0x00, 0x01, 0x02, 0x03, 0x04],
4297 num_beats: 16,
4298 meter: 4,
4299 tempo: 120.5,
4300 };
4301 let bytes = acid.to_bytes();
4302 #[rustfmt::skip]
4303 let expected: [u8; 24] = [
4304 0x06, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x00, 0x01, 0x02, 0x03, 0x04, 0x10, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF1, 0x42, ];
4311 assert_eq!(bytes, expected);
4312 assert_eq!(AcidChunk::parse(&bytes), Some(acid));
4313 }
4314
4315 #[test]
4319 fn acid_full_metadata() {
4320 let acid = AcidChunk {
4321 flags: ACID_FLAG_ONE_SHOT | ACID_FLAG_ROOT_NOTE_SET | ACID_FLAG_HIGH_OCTAVE,
4322 root_note: 60, reserved: [0; 6],
4324 num_beats: 32,
4325 meter: 4,
4326 tempo: 95.0,
4327 };
4328 let bytes = wav_with_acid(&acid.to_bytes());
4329 let dmx = open_demux_from_bytes(bytes);
4330 let md: std::collections::HashMap<String, String> =
4331 dmx.metadata().iter().cloned().collect();
4332 assert_eq!(md.get("wav:acid.flags"), Some(&"0x00000013".to_string()));
4333 assert_eq!(md.get("wav:acid.one_shot"), Some(&"1".to_string()));
4334 assert_eq!(md.get("wav:acid.root_note_set"), Some(&"1".to_string()));
4335 assert_eq!(md.get("wav:acid.stretch"), Some(&"0".to_string()));
4336 assert_eq!(md.get("wav:acid.disk_based"), Some(&"0".to_string()));
4337 assert_eq!(md.get("wav:acid.high_octave"), Some(&"1".to_string()));
4338 assert_eq!(md.get("wav:acid.root_note"), Some(&"60".to_string()));
4339 assert_eq!(
4340 md.get("wav:acid.root_note_name"),
4341 Some(&"High C".to_string())
4342 );
4343 assert_eq!(md.get("wav:acid.num_beats"), Some(&"32".to_string()));
4344 assert_eq!(md.get("wav:acid.meter"), Some(&"4".to_string()));
4345 assert_eq!(md.get("wav:acid.tempo"), Some(&"95".to_string()));
4346 assert_eq!(md.get("wav:acid.reserved"), None);
4349 assert_eq!(md.get("wav:acid.body_len"), None);
4350 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
4351 }
4352
4353 #[test]
4357 fn acid_out_of_table_root_note_and_extras() {
4358 let acid = AcidChunk {
4359 flags: 0,
4360 root_note: 0,
4361 reserved: [0xAA, 0, 0, 0, 0, 0xBB],
4362 num_beats: 8,
4363 meter: 4,
4364 tempo: 133.25,
4365 };
4366 assert_eq!(acid.root_note_name(), None);
4367 let mut body = acid.to_bytes().to_vec();
4368 body.extend_from_slice(&[0xEE, 0xFF]); let bytes = wav_with_acid(&body);
4370 let dmx = open_demux_from_bytes(bytes);
4371 let md: std::collections::HashMap<String, String> =
4372 dmx.metadata().iter().cloned().collect();
4373 assert_eq!(md.get("wav:acid.root_note"), Some(&"0".to_string()));
4374 assert_eq!(md.get("wav:acid.root_note_name"), None);
4375 assert_eq!(md.get("wav:acid.tempo"), Some(&"133.25".to_string()));
4376 assert_eq!(
4377 md.get("wav:acid.reserved"),
4378 Some(&"AA00000000BB".to_string())
4379 );
4380 assert_eq!(md.get("wav:acid.body_len"), Some(&"26".to_string()));
4381 }
4382
4383 #[test]
4386 fn acid_truncated_is_skipped() {
4387 let bytes = wav_with_acid(&[0u8; 23]);
4388 let dmx = open_demux_from_bytes(bytes);
4389 assert!(!dmx
4390 .metadata()
4391 .iter()
4392 .any(|(k, _)| k.starts_with("wav:acid")));
4393 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
4394 }
4395
4396 #[test]
4401 fn acid_round_trip() {
4402 let acid = AcidChunk {
4403 flags: ACID_FLAG_ROOT_NOTE_SET,
4404 root_note: 50, reserved: [0; 6],
4406 num_beats: 64,
4407 meter: 4,
4408 tempo: 174.0,
4409 };
4410 let payload: Vec<u8> = (0..400u32).flat_map(|i| (i as i16).to_le_bytes()).collect();
4411 let stream = make_stream(SampleFormat::S16, 1, 44_100);
4412 let opts = WavMuxOptions::default().with_acid(acid);
4413 let bytes = mux_to_bytes(&stream, &payload, opts, "acid-rt");
4414 let mut chunk = b"acid".to_vec();
4416 chunk.extend_from_slice(&24u32.to_le_bytes());
4417 chunk.extend_from_slice(&acid.to_bytes());
4418 assert!(bytes.windows(chunk.len()).any(|w| w == &chunk[..]));
4419
4420 let mut dmx = open_demux_from_bytes(bytes);
4421 let md: std::collections::HashMap<String, String> =
4422 dmx.metadata().iter().cloned().collect();
4423 assert_eq!(md.get("wav:acid.root_note"), Some(&"50".to_string()));
4424 assert_eq!(md.get("wav:acid.root_note_name"), Some(&"D".to_string()));
4425 assert_eq!(md.get("wav:acid.num_beats"), Some(&"64".to_string()));
4426 assert_eq!(md.get("wav:acid.tempo"), Some(&"174".to_string()));
4427 let mut out = Vec::new();
4428 loop {
4429 match dmx.next_packet() {
4430 Ok(p) => out.extend_from_slice(&p.data),
4431 Err(Error::Eof) => break,
4432 Err(e) => panic!("demux error: {e}"),
4433 }
4434 }
4435 assert_eq!(out, payload);
4436 }
4437
4438 #[test]
4443 fn acid_typed_accessor() {
4444 let acid = AcidChunk {
4445 flags: ACID_FLAG_ONE_SHOT | ACID_FLAG_DISK_BASED,
4446 root_note: 71, reserved: [1, 2, 3, 4, 5, 6],
4448 num_beats: 4,
4449 meter: 3,
4450 tempo: 60.0,
4451 };
4452 let bytes = wav_with_acid(&acid.to_bytes());
4453 use std::io::Cursor;
4454 let dmx = open_wav_demuxer(Box::new(Cursor::new(bytes))).unwrap();
4455 assert_eq!(dmx.acid(), Some(&acid));
4456 let got = dmx.acid().unwrap();
4457 assert!(got.one_shot() && got.disk_based());
4458 assert!(!got.root_note_set() && !got.stretch() && !got.high_octave());
4459 assert_eq!(got.root_note_name(), Some("High B"));
4460 let md: std::collections::HashMap<String, String> =
4461 dmx.metadata().iter().cloned().collect();
4462 assert_eq!(
4463 md.get("wav:acid.reserved"),
4464 Some(&"010203040506".to_string())
4465 );
4466
4467 let plain = wav_with_smpl_and_inst(None, None);
4469 let dmx = open_wav_demuxer(Box::new(Cursor::new(plain))).unwrap();
4470 assert_eq!(dmx.acid(), None);
4471 }
4472
4473 fn wav_with_plst(plst_body: &[u8]) -> Vec<u8> {
4479 let mut buf = Vec::new();
4480 buf.extend_from_slice(b"RIFF");
4481 buf.extend_from_slice(&0u32.to_le_bytes());
4482 buf.extend_from_slice(b"WAVE");
4483 buf.extend_from_slice(b"fmt ");
4485 buf.extend_from_slice(&16u32.to_le_bytes());
4486 buf.extend_from_slice(&FMT_PCM.to_le_bytes());
4487 buf.extend_from_slice(&1u16.to_le_bytes());
4488 buf.extend_from_slice(&8_000u32.to_le_bytes());
4489 buf.extend_from_slice(&16_000u32.to_le_bytes());
4490 buf.extend_from_slice(&2u16.to_le_bytes());
4491 buf.extend_from_slice(&16u16.to_le_bytes());
4492 buf.extend_from_slice(b"plst");
4494 buf.extend_from_slice(&(plst_body.len() as u32).to_le_bytes());
4495 buf.extend_from_slice(plst_body);
4496 if plst_body.len() % 2 == 1 {
4497 buf.push(0);
4498 }
4499 buf.extend_from_slice(b"data");
4501 buf.extend_from_slice(&0u32.to_le_bytes());
4502 buf
4503 }
4504
4505 fn plst_segment(dw_name: u32, dw_length: u32, dw_loops: u32) -> Vec<u8> {
4508 let mut b = Vec::with_capacity(12);
4509 b.extend_from_slice(&dw_name.to_le_bytes());
4510 b.extend_from_slice(&dw_length.to_le_bytes());
4511 b.extend_from_slice(&dw_loops.to_le_bytes());
4512 b
4513 }
4514
4515 #[test]
4520 fn plst_full_metadata() {
4521 let mut plst_body = Vec::new();
4522 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);
4528 let dmx = open_demux_from_bytes(bytes);
4529 let md: std::collections::HashMap<String, String> =
4530 dmx.metadata().iter().cloned().collect();
4531
4532 assert_eq!(md.get("wav:plst.count"), Some(&"3".to_string()));
4533 assert_eq!(md.get("wav:plst.0.cue_id"), Some(&"1".to_string()));
4534 assert_eq!(md.get("wav:plst.0.length"), Some(&"4410".to_string()));
4535 assert_eq!(md.get("wav:plst.0.loops"), Some(&"1".to_string()));
4536 assert_eq!(md.get("wav:plst.1.cue_id"), Some(&"2".to_string()));
4537 assert_eq!(md.get("wav:plst.1.length"), Some(&"8820".to_string()));
4538 assert_eq!(md.get("wav:plst.1.loops"), Some(&"2".to_string()));
4539 assert_eq!(md.get("wav:plst.2.cue_id"), Some(&"1".to_string()));
4540 assert_eq!(md.get("wav:plst.2.length"), Some(&"4410".to_string()));
4541 assert_eq!(md.get("wav:plst.2.loops"), Some(&"1".to_string()));
4542 }
4543
4544 #[test]
4549 fn plst_truncated_count_is_clamped() {
4550 let mut plst_body = Vec::new();
4552 plst_body.extend_from_slice(&10u32.to_le_bytes());
4553 plst_body.extend(plst_segment(42, 1000, 1));
4554 let bytes = wav_with_plst(&plst_body);
4555 let dmx = open_demux_from_bytes(bytes);
4556 let md: std::collections::HashMap<String, String> =
4557 dmx.metadata().iter().cloned().collect();
4558 assert_eq!(md.get("wav:plst.count"), Some(&"1".to_string()));
4559 assert_eq!(md.get("wav:plst.0.cue_id"), Some(&"42".to_string()));
4560 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
4562 }
4563
4564 #[test]
4568 fn plst_truncated_header_is_opaque() {
4569 let plst_body = vec![0u8, 0]; let bytes = wav_with_plst(&plst_body);
4571 let dmx = open_demux_from_bytes(bytes);
4572 let md: std::collections::HashMap<String, String> =
4573 dmx.metadata().iter().cloned().collect();
4574 assert!(md.keys().all(|k| !k.starts_with("wav:plst.")));
4575 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
4576 }
4577
4578 #[test]
4581 fn plst_zero_segments() {
4582 let plst_body = 0u32.to_le_bytes().to_vec();
4583 let bytes = wav_with_plst(&plst_body);
4584 let dmx = open_demux_from_bytes(bytes);
4585 let md: std::collections::HashMap<String, String> =
4586 dmx.metadata().iter().cloned().collect();
4587 assert_eq!(md.get("wav:plst.count"), Some(&"0".to_string()));
4588 assert!(md
4589 .keys()
4590 .all(|k| !k.starts_with("wav:plst.") || k == "wav:plst.count"));
4591 }
4592
4593 #[test]
4596 fn plst_odd_body_padding() {
4597 let mut plst_body = Vec::new();
4599 plst_body.extend_from_slice(&1u32.to_le_bytes());
4600 plst_body.extend(plst_segment(5, 100, 1));
4601 plst_body.push(0xAA);
4602 let bytes = wav_with_plst(&plst_body);
4603 let dmx = open_demux_from_bytes(bytes);
4604 let md: std::collections::HashMap<String, String> =
4605 dmx.metadata().iter().cloned().collect();
4606 assert_eq!(md.get("wav:plst.count"), Some(&"1".to_string()));
4607 assert_eq!(md.get("wav:plst.0.cue_id"), Some(&"5".to_string()));
4608 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
4609 }
4610
4611 fn wav_with_fact(fact_body: &[u8], data_payload: &[u8]) -> Vec<u8> {
4617 let mut buf = Vec::new();
4618 buf.extend_from_slice(b"RIFF");
4619 buf.extend_from_slice(&0u32.to_le_bytes());
4620 buf.extend_from_slice(b"WAVE");
4621 buf.extend_from_slice(b"fmt ");
4623 buf.extend_from_slice(&16u32.to_le_bytes());
4624 buf.extend_from_slice(&FMT_PCM.to_le_bytes());
4625 buf.extend_from_slice(&1u16.to_le_bytes());
4626 buf.extend_from_slice(&8_000u32.to_le_bytes());
4627 buf.extend_from_slice(&16_000u32.to_le_bytes());
4628 buf.extend_from_slice(&2u16.to_le_bytes());
4629 buf.extend_from_slice(&16u16.to_le_bytes());
4630 buf.extend_from_slice(b"fact");
4632 buf.extend_from_slice(&(fact_body.len() as u32).to_le_bytes());
4633 buf.extend_from_slice(fact_body);
4634 if fact_body.len() % 2 == 1 {
4635 buf.push(0);
4636 }
4637 buf.extend_from_slice(b"data");
4639 buf.extend_from_slice(&(data_payload.len() as u32).to_le_bytes());
4640 buf.extend_from_slice(data_payload);
4641 if data_payload.len() % 2 == 1 {
4642 buf.push(0);
4643 }
4644 buf
4645 }
4646
4647 #[test]
4655 fn fact_minimum_body_matches_data() {
4656 let payload: Vec<u8> = (0..200u32).map(|i| (i & 0xFF) as u8).collect();
4658 let fact_body = 100u32.to_le_bytes().to_vec();
4659 let bytes = wav_with_fact(&fact_body, &payload);
4660 let dmx = open_demux_from_bytes(bytes);
4661 let md: std::collections::HashMap<String, String> =
4662 dmx.metadata().iter().cloned().collect();
4663 assert_eq!(md.get("wav:fact.sample_count"), Some(&"100".to_string()));
4664 assert!(!md.contains_key("wav:fact.mismatch"));
4665 assert!(!md.contains_key("wav:fact.body_len"));
4666 assert_eq!(dmx.streams()[0].duration, Some(100));
4669 }
4670
4671 #[test]
4677 fn fact_mismatch_surfaces_diagnostic_and_overrides_duration() {
4678 let payload: Vec<u8> = (0..200u32).map(|i| (i & 0xFF) as u8).collect();
4680 let fact_body = 50u32.to_le_bytes().to_vec();
4681 let bytes = wav_with_fact(&fact_body, &payload);
4682 let dmx = open_demux_from_bytes(bytes);
4683 let md: std::collections::HashMap<String, String> =
4684 dmx.metadata().iter().cloned().collect();
4685 assert_eq!(md.get("wav:fact.sample_count"), Some(&"50".to_string()));
4686 assert_eq!(
4687 md.get("wav:fact.mismatch"),
4688 Some(&"block_samples=100 fact_samples=50".to_string())
4689 );
4690 assert_eq!(dmx.streams()[0].duration, Some(50));
4691 }
4692
4693 #[test]
4701 fn fact_extension_bytes_preserved_in_body_len() {
4702 let payload: Vec<u8> = (0..200u32).map(|i| (i & 0xFF) as u8).collect();
4705 let mut fact_body = 100u32.to_le_bytes().to_vec();
4706 fact_body.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03, 0x04]);
4707 let bytes = wav_with_fact(&fact_body, &payload);
4708 let dmx = open_demux_from_bytes(bytes);
4709 let md: std::collections::HashMap<String, String> =
4710 dmx.metadata().iter().cloned().collect();
4711 assert_eq!(md.get("wav:fact.sample_count"), Some(&"100".to_string()));
4712 assert_eq!(md.get("wav:fact.body_len"), Some(&"12".to_string()));
4713 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
4716 }
4717
4718 #[test]
4724 fn fact_truncated_body_is_opaque() {
4725 let payload: Vec<u8> = vec![0u8, 0u8];
4726 let fact_body = vec![0u8, 0u8]; let bytes = wav_with_fact(&fact_body, &payload);
4728 let dmx = open_demux_from_bytes(bytes);
4729 let md: std::collections::HashMap<String, String> =
4730 dmx.metadata().iter().cloned().collect();
4731 assert!(md.keys().all(|k| !k.starts_with("wav:fact.")));
4732 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
4733 }
4734
4735 #[test]
4742 fn fact_odd_body_padding() {
4743 let payload: Vec<u8> = vec![0xAA, 0x55];
4745 let mut fact_body = 1u32.to_le_bytes().to_vec();
4746 fact_body.push(0x42);
4747 let bytes = wav_with_fact(&fact_body, &payload);
4748 let dmx = open_demux_from_bytes(bytes);
4749 let md: std::collections::HashMap<String, String> =
4750 dmx.metadata().iter().cloned().collect();
4751 assert_eq!(md.get("wav:fact.sample_count"), Some(&"1".to_string()));
4752 assert_eq!(md.get("wav:fact.body_len"), Some(&"5".to_string()));
4753 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
4754 }
4755
4756 #[test]
4764 fn fact_chunk_round_trip_alaw_mono() {
4765 let payload: Vec<u8> = (0..=255u8).collect(); let stream = make_g711_stream("pcm_alaw", 1, 8_000);
4767 let bytes = mux_to_bytes(&stream, &payload, WavMuxOptions::default(), "alaw-fact");
4768 assert!(
4770 find_chunk(&bytes, b"fact").is_some(),
4771 "muxer must emit fact chunk for non-PCM wFormatTag"
4772 );
4773 let dmx = open_demux_from_bytes(bytes);
4774 let md: std::collections::HashMap<String, String> =
4775 dmx.metadata().iter().cloned().collect();
4776 assert_eq!(md.get("wav:fact.sample_count"), Some(&"256".to_string()));
4777 assert!(!md.contains_key("wav:fact.mismatch"));
4778 assert_eq!(dmx.streams()[0].duration, Some(256));
4779 }
4780
4781 #[test]
4788 fn fact_chunk_not_emitted_for_pcm() {
4789 let samples: Vec<i16> = (0..100).map(|i| (i * 100) as i16).collect();
4790 let mut payload = Vec::with_capacity(samples.len() * 2);
4791 for s in &samples {
4792 payload.extend_from_slice(&s.to_le_bytes());
4793 }
4794 let stream = make_stream(SampleFormat::S16, 1, 8_000);
4795 let bytes = mux_to_bytes(&stream, &payload, WavMuxOptions::default(), "pcm-no-fact");
4796 assert!(
4797 find_chunk(&bytes, b"fact").is_none(),
4798 "PCM muxer output must not carry a fact chunk"
4799 );
4800 }
4801
4802 #[test]
4808 fn fact_chunk_round_trip_extensible() {
4809 let samples: Vec<i16> = (0..200).map(|i| (i * 50) as i16).collect();
4810 let mut payload = Vec::with_capacity(samples.len() * 2);
4811 for s in &samples {
4812 payload.extend_from_slice(&s.to_le_bytes());
4813 }
4814 let stream = make_stream(SampleFormat::S16, 1, 8_000);
4815 let opts = WavMuxOptions::default().with_extensible(0x4); let bytes = mux_to_bytes(&stream, &payload, opts, "ext-fact");
4817 assert!(
4818 find_chunk(&bytes, b"fact").is_some(),
4819 "EXTENSIBLE muxer output must carry a fact chunk"
4820 );
4821 let dmx = open_demux_from_bytes(bytes);
4822 let md: std::collections::HashMap<String, String> =
4823 dmx.metadata().iter().cloned().collect();
4824 assert_eq!(md.get("wav:fact.sample_count"), Some(&"200".to_string()));
4825 assert!(!md.contains_key("wav:fact.mismatch"));
4826 }
4827
4828 fn wav_with_ixml(ixml_body: &[u8]) -> Vec<u8> {
4833 let mut buf = Vec::new();
4834 buf.extend_from_slice(b"RIFF");
4835 buf.extend_from_slice(&0u32.to_le_bytes());
4836 buf.extend_from_slice(b"WAVE");
4837 buf.extend_from_slice(b"fmt ");
4839 buf.extend_from_slice(&16u32.to_le_bytes());
4840 buf.extend_from_slice(&FMT_PCM.to_le_bytes());
4841 buf.extend_from_slice(&1u16.to_le_bytes());
4842 buf.extend_from_slice(&8_000u32.to_le_bytes());
4843 buf.extend_from_slice(&16_000u32.to_le_bytes());
4844 buf.extend_from_slice(&2u16.to_le_bytes());
4845 buf.extend_from_slice(&16u16.to_le_bytes());
4846 buf.extend_from_slice(b"iXML");
4848 buf.extend_from_slice(&(ixml_body.len() as u32).to_le_bytes());
4849 buf.extend_from_slice(ixml_body);
4850 if ixml_body.len() % 2 == 1 {
4851 buf.push(0);
4852 }
4853 buf.extend_from_slice(b"data");
4855 buf.extend_from_slice(&0u32.to_le_bytes());
4856 buf
4857 }
4858
4859 #[test]
4864 fn ixml_canonical_document_round_trips() {
4865 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
4866<BWFXML>
4867 <IXML_VERSION>2.10</IXML_VERSION>
4868 <PROJECT>OxideAV Round 205</PROJECT>
4869 <SCENE>scn-001</SCENE>
4870 <TAKE>1</TAKE>
4871 <NOTE>iXML canonical fixture</NOTE>
4872 <TRACK_LIST>
4873 <TRACK_COUNT>1</TRACK_COUNT>
4874 <TRACK>
4875 <CHANNEL_INDEX>1</CHANNEL_INDEX>
4876 <NAME>Boom</NAME>
4877 <FUNCTION>Dialog</FUNCTION>
4878 </TRACK>
4879 </TRACK_LIST>
4880</BWFXML>"#;
4881 let bytes = wav_with_ixml(xml.as_bytes());
4882 let dmx = open_demux_from_bytes(bytes);
4883 let md: std::collections::HashMap<String, String> =
4884 dmx.metadata().iter().cloned().collect();
4885 assert_eq!(
4886 md.get("wav:ixml.body_len"),
4887 Some(&xml.len().to_string()),
4888 "raw chunk-body length must surface verbatim"
4889 );
4890 assert_eq!(
4891 md.get("wav:ixml").map(|s| s.as_str()),
4892 Some(xml.trim()),
4893 "iXML text payload must round-trip"
4894 );
4895 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
4896 }
4897
4898 #[test]
4904 fn ixml_trailing_nuls_trimmed_in_text_but_body_len_kept() {
4905 let mut body = b"<BWFXML><PROJECT>OAV</PROJECT></BWFXML>".to_vec();
4906 body.resize(body.len() + 64, 0);
4909 let bytes = wav_with_ixml(&body);
4910 let dmx = open_demux_from_bytes(bytes);
4911 let md: std::collections::HashMap<String, String> =
4912 dmx.metadata().iter().cloned().collect();
4913 assert_eq!(
4914 md.get("wav:ixml"),
4915 Some(&"<BWFXML><PROJECT>OAV</PROJECT></BWFXML>".to_string())
4916 );
4917 assert_eq!(md.get("wav:ixml.body_len"), Some(&body.len().to_string()));
4918 }
4919
4920 #[test]
4925 fn ixml_empty_body_surfaces_only_body_len() {
4926 let bytes = wav_with_ixml(&[]);
4927 let dmx = open_demux_from_bytes(bytes);
4928 let md: std::collections::HashMap<String, String> =
4929 dmx.metadata().iter().cloned().collect();
4930 assert_eq!(md.get("wav:ixml.body_len"), Some(&"0".to_string()));
4931 assert!(!md.contains_key("wav:ixml"));
4932 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
4933 }
4934
4935 #[test]
4938 fn ixml_whitespace_only_body_omits_text_key() {
4939 let body = b" \t\r\n \0\0\0".to_vec();
4940 let bytes = wav_with_ixml(&body);
4941 let dmx = open_demux_from_bytes(bytes);
4942 let md: std::collections::HashMap<String, String> =
4943 dmx.metadata().iter().cloned().collect();
4944 assert_eq!(md.get("wav:ixml.body_len"), Some(&body.len().to_string()));
4945 assert!(!md.contains_key("wav:ixml"));
4946 }
4947
4948 #[test]
4952 fn ixml_odd_body_padding() {
4953 let body = b"<X>1</X><Y>2</Y>!".to_vec();
4956 assert_eq!(body.len() % 2, 1);
4957 let bytes = wav_with_ixml(&body);
4958 let dmx = open_demux_from_bytes(bytes);
4959 let md: std::collections::HashMap<String, String> =
4960 dmx.metadata().iter().cloned().collect();
4961 assert_eq!(md.get("wav:ixml"), Some(&"<X>1</X><Y>2</Y>!".to_string()));
4962 assert_eq!(md.get("wav:ixml.body_len"), Some(&"17".to_string()));
4963 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
4964 }
4965
4966 fn wav_with_axml(axml_body: &[u8]) -> Vec<u8> {
4971 let mut buf = Vec::new();
4972 buf.extend_from_slice(b"RIFF");
4973 buf.extend_from_slice(&0u32.to_le_bytes());
4974 buf.extend_from_slice(b"WAVE");
4975 buf.extend_from_slice(b"fmt ");
4977 buf.extend_from_slice(&16u32.to_le_bytes());
4978 buf.extend_from_slice(&FMT_PCM.to_le_bytes());
4979 buf.extend_from_slice(&1u16.to_le_bytes());
4980 buf.extend_from_slice(&8_000u32.to_le_bytes());
4981 buf.extend_from_slice(&16_000u32.to_le_bytes());
4982 buf.extend_from_slice(&2u16.to_le_bytes());
4983 buf.extend_from_slice(&16u16.to_le_bytes());
4984 buf.extend_from_slice(b"axml");
4986 buf.extend_from_slice(&(axml_body.len() as u32).to_le_bytes());
4987 buf.extend_from_slice(axml_body);
4988 if axml_body.len() % 2 == 1 {
4989 buf.push(0);
4990 }
4991 buf.extend_from_slice(b"data");
4993 buf.extend_from_slice(&0u32.to_le_bytes());
4994 buf
4995 }
4996
4997 #[test]
5006 fn axml_canonical_ebucore_adm_document_round_trips() {
5007 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
5008<ebuCoreMain xmlns="urn:ebu:metadata-schema:ebucore"
5009 xmlns:dc="http://purl.org/dc/elements/1.1/">
5010 <coreMetadata>
5011 <format>
5012 <audioFormatExtended>
5013 <audioProgramme audioProgrammeID="APR_1001"
5014 audioProgrammeName="OxideAV Round 258 demo">
5015 <audioContentIDRef>ACO_1001</audioContentIDRef>
5016 </audioProgramme>
5017 </audioFormatExtended>
5018 </format>
5019 </coreMetadata>
5020</ebuCoreMain>"#;
5021 let bytes = wav_with_axml(xml.as_bytes());
5022 let dmx = open_demux_from_bytes(bytes);
5023 let md: std::collections::HashMap<String, String> =
5024 dmx.metadata().iter().cloned().collect();
5025 assert_eq!(
5026 md.get("wav:axml.body_len"),
5027 Some(&xml.len().to_string()),
5028 "raw chunk-body length must surface verbatim"
5029 );
5030 assert_eq!(
5031 md.get("wav:axml").map(|s| s.as_str()),
5032 Some(xml.trim()),
5033 "axml text payload must round-trip"
5034 );
5035 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
5038 }
5039
5040 #[test]
5043 fn axml_isrc_identifier_document_round_trips() {
5044 let xml = r#"<ebuCoreMain xmlns:dc="http://purl.org/dc/elements/1.1/"
5045 xmlns="urn:ebu:metadata-schema:ebucore">
5046 <coreMetadata>
5047 <identifier typeLabel="GUID" typeDefinition="Globally Unique Identifier"
5048 formatLabel="ISRC" formatDefinition="International Standard Recording Code">
5049 <dc:identifier>ISRC:NOX001212345</dc:identifier>
5050 </identifier>
5051 </coreMetadata>
5052</ebuCoreMain>"#;
5053 let bytes = wav_with_axml(xml.as_bytes());
5054 let dmx = open_demux_from_bytes(bytes);
5055 let md: std::collections::HashMap<String, String> =
5056 dmx.metadata().iter().cloned().collect();
5057 assert_eq!(md.get("wav:axml").map(|s| s.as_str()), Some(xml.trim()));
5058 assert!(
5059 md.get("wav:axml")
5060 .map(|s| s.contains("ISRC:NOX001212345"))
5061 .unwrap_or(false),
5062 "ISRC identifier must survive the round-trip"
5063 );
5064 }
5065
5066 #[test]
5073 fn axml_trailing_nuls_trimmed_in_text_but_body_len_kept() {
5074 let mut body = b"<ebuCoreMain><id>X</id></ebuCoreMain>".to_vec();
5075 body.resize(body.len() + 128, 0);
5076 let bytes = wav_with_axml(&body);
5077 let dmx = open_demux_from_bytes(bytes);
5078 let md: std::collections::HashMap<String, String> =
5079 dmx.metadata().iter().cloned().collect();
5080 assert_eq!(
5081 md.get("wav:axml"),
5082 Some(&"<ebuCoreMain><id>X</id></ebuCoreMain>".to_string())
5083 );
5084 assert_eq!(md.get("wav:axml.body_len"), Some(&body.len().to_string()));
5085 }
5086
5087 #[test]
5095 fn axml_empty_body_surfaces_only_body_len() {
5096 let bytes = wav_with_axml(&[]);
5097 let dmx = open_demux_from_bytes(bytes);
5098 let md: std::collections::HashMap<String, String> =
5099 dmx.metadata().iter().cloned().collect();
5100 assert_eq!(md.get("wav:axml.body_len"), Some(&"0".to_string()));
5101 assert!(!md.contains_key("wav:axml"));
5102 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
5103 }
5104
5105 #[test]
5109 fn axml_whitespace_only_body_omits_text_key() {
5110 let body = b" \t\r\n \0\0\0".to_vec();
5111 let bytes = wav_with_axml(&body);
5112 let dmx = open_demux_from_bytes(bytes);
5113 let md: std::collections::HashMap<String, String> =
5114 dmx.metadata().iter().cloned().collect();
5115 assert_eq!(md.get("wav:axml.body_len"), Some(&body.len().to_string()));
5116 assert!(!md.contains_key("wav:axml"));
5117 }
5118
5119 #[test]
5123 fn axml_odd_body_padding() {
5124 let body = b"<root><id>z</id></root>!!".to_vec();
5128 assert_eq!(body.len() % 2, 1);
5129 let bytes = wav_with_axml(&body);
5130 let dmx = open_demux_from_bytes(bytes);
5131 let md: std::collections::HashMap<String, String> =
5132 dmx.metadata().iter().cloned().collect();
5133 assert_eq!(
5134 md.get("wav:axml"),
5135 Some(&"<root><id>z</id></root>!!".to_string())
5136 );
5137 assert_eq!(md.get("wav:axml.body_len"), Some(&"25".to_string()));
5138 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
5139 }
5140
5141 fn wav_with_pmx(pmx_body: &[u8]) -> Vec<u8> {
5148 let mut buf = Vec::new();
5149 buf.extend_from_slice(b"RIFF");
5150 buf.extend_from_slice(&0u32.to_le_bytes());
5151 buf.extend_from_slice(b"WAVE");
5152 buf.extend_from_slice(b"fmt ");
5154 buf.extend_from_slice(&16u32.to_le_bytes());
5155 buf.extend_from_slice(&FMT_PCM.to_le_bytes());
5156 buf.extend_from_slice(&1u16.to_le_bytes());
5157 buf.extend_from_slice(&8_000u32.to_le_bytes());
5158 buf.extend_from_slice(&16_000u32.to_le_bytes());
5159 buf.extend_from_slice(&2u16.to_le_bytes());
5160 buf.extend_from_slice(&16u16.to_le_bytes());
5161 buf.extend_from_slice(b"_PMX");
5163 buf.extend_from_slice(&(pmx_body.len() as u32).to_le_bytes());
5164 buf.extend_from_slice(pmx_body);
5165 if pmx_body.len() % 2 == 1 {
5166 buf.push(0);
5167 }
5168 buf.extend_from_slice(b"data");
5170 buf.extend_from_slice(&0u32.to_le_bytes());
5171 buf
5172 }
5173
5174 #[test]
5181 fn pmx_canonical_xmp_packet_round_trips() {
5182 let xml = r#"<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
5183<x:xmpmeta xmlns:x="adobe:ns:meta/">
5184 <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
5185 <rdf:Description rdf:about=""
5186 xmlns:dc="http://purl.org/dc/elements/1.1/">
5187 <dc:title>
5188 <rdf:Alt>
5189 <rdf:li xml:lang="x-default">OxideAV Round 263 Fixture</rdf:li>
5190 </rdf:Alt>
5191 </dc:title>
5192 </rdf:Description>
5193 </rdf:RDF>
5194</x:xmpmeta>
5195<?xpacket end="w"?>"#;
5196 let bytes = wav_with_pmx(xml.as_bytes());
5197 let dmx = open_demux_from_bytes(bytes);
5198 let md: std::collections::HashMap<String, String> =
5199 dmx.metadata().iter().cloned().collect();
5200 assert_eq!(
5201 md.get("wav:xmp.body_len"),
5202 Some(&xml.len().to_string()),
5203 "raw chunk-body length must surface verbatim"
5204 );
5205 assert_eq!(
5206 md.get("wav:xmp").map(|s| s.as_str()),
5207 Some(xml.trim()),
5208 "XMP packet text must round-trip"
5209 );
5210 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
5211 }
5212
5213 #[test]
5218 fn pmx_trailing_nuls_trimmed_in_text_but_body_len_kept() {
5219 let mut body = b"<x:xmpmeta xmlns:x=\"adobe:ns:meta/\"/>".to_vec();
5220 body.resize(body.len() + 96, 0);
5223 let bytes = wav_with_pmx(&body);
5224 let dmx = open_demux_from_bytes(bytes);
5225 let md: std::collections::HashMap<String, String> =
5226 dmx.metadata().iter().cloned().collect();
5227 assert_eq!(
5228 md.get("wav:xmp"),
5229 Some(&"<x:xmpmeta xmlns:x=\"adobe:ns:meta/\"/>".to_string())
5230 );
5231 assert_eq!(md.get("wav:xmp.body_len"), Some(&body.len().to_string()));
5232 }
5233
5234 #[test]
5239 fn pmx_empty_body_surfaces_only_body_len() {
5240 let bytes = wav_with_pmx(&[]);
5241 let dmx = open_demux_from_bytes(bytes);
5242 let md: std::collections::HashMap<String, String> =
5243 dmx.metadata().iter().cloned().collect();
5244 assert_eq!(md.get("wav:xmp.body_len"), Some(&"0".to_string()));
5245 assert!(!md.contains_key("wav:xmp"));
5246 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
5247 }
5248
5249 #[test]
5253 fn pmx_whitespace_only_body_omits_text_key() {
5254 let body = b" \t\r\n \0\0\0".to_vec();
5255 let bytes = wav_with_pmx(&body);
5256 let dmx = open_demux_from_bytes(bytes);
5257 let md: std::collections::HashMap<String, String> =
5258 dmx.metadata().iter().cloned().collect();
5259 assert_eq!(md.get("wav:xmp.body_len"), Some(&body.len().to_string()));
5260 assert!(!md.contains_key("wav:xmp"));
5261 }
5262
5263 #[test]
5267 fn pmx_odd_body_padding() {
5268 let body = b"<x:xmpmeta xmlns:x=\"a:n\"/>!".to_vec();
5270 assert_eq!(body.len() % 2, 1);
5271 let bytes = wav_with_pmx(&body);
5272 let dmx = open_demux_from_bytes(bytes);
5273 let md: std::collections::HashMap<String, String> =
5274 dmx.metadata().iter().cloned().collect();
5275 assert_eq!(
5276 md.get("wav:xmp"),
5277 Some(&"<x:xmpmeta xmlns:x=\"a:n\"/>!".to_string())
5278 );
5279 assert_eq!(md.get("wav:xmp.body_len"), Some(&"27".to_string()));
5280 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
5281 }
5282
5283 #[test]
5287 fn pmx_absent_chunk_emits_no_xmp_keys() {
5288 let mut buf = Vec::new();
5293 buf.extend_from_slice(b"RIFF");
5294 buf.extend_from_slice(&0u32.to_le_bytes());
5295 buf.extend_from_slice(b"WAVE");
5296 buf.extend_from_slice(b"fmt ");
5297 buf.extend_from_slice(&16u32.to_le_bytes());
5298 buf.extend_from_slice(&FMT_PCM.to_le_bytes());
5299 buf.extend_from_slice(&1u16.to_le_bytes());
5300 buf.extend_from_slice(&8_000u32.to_le_bytes());
5301 buf.extend_from_slice(&16_000u32.to_le_bytes());
5302 buf.extend_from_slice(&2u16.to_le_bytes());
5303 buf.extend_from_slice(&16u16.to_le_bytes());
5304 buf.extend_from_slice(b"data");
5305 buf.extend_from_slice(&0u32.to_le_bytes());
5306 let dmx = open_demux_from_bytes(buf);
5307 let md: std::collections::HashMap<String, String> =
5308 dmx.metadata().iter().cloned().collect();
5309 assert!(!md.contains_key("wav:xmp"));
5310 assert!(!md.contains_key("wav:xmp.body_len"));
5311 }
5312
5313 fn wav_with_cset(cset_body: &[u8]) -> Vec<u8> {
5319 let mut buf = Vec::new();
5320 buf.extend_from_slice(b"RIFF");
5321 buf.extend_from_slice(&0u32.to_le_bytes());
5322 buf.extend_from_slice(b"WAVE");
5323 buf.extend_from_slice(b"fmt ");
5325 buf.extend_from_slice(&16u32.to_le_bytes());
5326 buf.extend_from_slice(&FMT_PCM.to_le_bytes());
5327 buf.extend_from_slice(&1u16.to_le_bytes());
5328 buf.extend_from_slice(&8_000u32.to_le_bytes());
5329 buf.extend_from_slice(&16_000u32.to_le_bytes());
5330 buf.extend_from_slice(&2u16.to_le_bytes());
5331 buf.extend_from_slice(&16u16.to_le_bytes());
5332 buf.extend_from_slice(b"CSET");
5334 buf.extend_from_slice(&(cset_body.len() as u32).to_le_bytes());
5335 buf.extend_from_slice(cset_body);
5336 if cset_body.len() % 2 == 1 {
5337 buf.push(0);
5338 }
5339 buf.extend_from_slice(b"data");
5341 buf.extend_from_slice(&0u32.to_le_bytes());
5342 buf
5343 }
5344
5345 fn cset_body(code_page: u16, country: u16, language: u16, dialect: u16) -> Vec<u8> {
5348 let mut body = Vec::with_capacity(8);
5349 body.extend_from_slice(&code_page.to_le_bytes());
5350 body.extend_from_slice(&country.to_le_bytes());
5351 body.extend_from_slice(&language.to_le_bytes());
5352 body.extend_from_slice(&dialect.to_le_bytes());
5353 body
5354 }
5355
5356 #[test]
5361 fn cset_canonical_uk_english_round_trips() {
5362 let body = cset_body(1252, 44, 9, 2);
5365 let bytes = wav_with_cset(&body);
5366 let dmx = open_demux_from_bytes(bytes);
5367 let md: std::collections::HashMap<String, String> =
5368 dmx.metadata().iter().cloned().collect();
5369 assert_eq!(md.get("wav:cset.body_len"), Some(&"8".to_string()));
5370 assert_eq!(md.get("wav:cset.code_page"), Some(&"1252".to_string()));
5371 assert_eq!(md.get("wav:cset.country"), Some(&"44".to_string()));
5372 assert_eq!(
5373 md.get("wav:cset.country_name"),
5374 Some(&"United Kingdom".to_string())
5375 );
5376 assert_eq!(md.get("wav:cset.language"), Some(&"9".to_string()));
5377 assert_eq!(md.get("wav:cset.dialect"), Some(&"2".to_string()));
5378 assert_eq!(
5379 md.get("wav:cset.language_name"),
5380 Some(&"UK English".to_string())
5381 );
5382 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
5383 }
5384
5385 #[test]
5390 fn cset_all_zero_uses_spec_defaults() {
5391 let body = cset_body(0, 0, 0, 0);
5392 let bytes = wav_with_cset(&body);
5393 let dmx = open_demux_from_bytes(bytes);
5394 let md: std::collections::HashMap<String, String> =
5395 dmx.metadata().iter().cloned().collect();
5396 assert_eq!(md.get("wav:cset.code_page"), Some(&"0".to_string()));
5397 assert_eq!(md.get("wav:cset.country"), Some(&"0".to_string()));
5398 assert_eq!(
5399 md.get("wav:cset.country_name"),
5400 Some(&"None".to_string()),
5401 "wCountryCode = 0 must resolve to the §3 'None' placeholder"
5402 );
5403 assert_eq!(md.get("wav:cset.language"), Some(&"0".to_string()));
5404 assert_eq!(md.get("wav:cset.dialect"), Some(&"0".to_string()));
5405 assert_eq!(
5406 md.get("wav:cset.language_name"),
5407 Some(&"None".to_string()),
5408 "wLanguageCode = 0 must resolve to the §3 'None' placeholder regardless of dialect"
5409 );
5410 }
5411
5412 #[test]
5417 fn cset_unknown_codes_emit_raw_values_only() {
5418 let body = cset_body(65001, 999, 99, 99);
5421 let bytes = wav_with_cset(&body);
5422 let dmx = open_demux_from_bytes(bytes);
5423 let md: std::collections::HashMap<String, String> =
5424 dmx.metadata().iter().cloned().collect();
5425 assert_eq!(md.get("wav:cset.code_page"), Some(&"65001".to_string()));
5426 assert_eq!(md.get("wav:cset.country"), Some(&"999".to_string()));
5427 assert!(
5428 !md.contains_key("wav:cset.country_name"),
5429 "unknown country must not synthesise a human-readable name"
5430 );
5431 assert_eq!(md.get("wav:cset.language"), Some(&"99".to_string()));
5432 assert_eq!(md.get("wav:cset.dialect"), Some(&"99".to_string()));
5433 assert!(
5434 !md.contains_key("wav:cset.language_name"),
5435 "unknown language pair must not synthesise a human-readable name"
5436 );
5437 }
5438
5439 #[test]
5443 fn cset_short_body_treated_as_opaque() {
5444 let body = vec![0x52, 0x04, 0x00, 0x00];
5448 let bytes = wav_with_cset(&body);
5449 let dmx = open_demux_from_bytes(bytes);
5450 let md: std::collections::HashMap<String, String> =
5451 dmx.metadata().iter().cloned().collect();
5452 assert_eq!(md.get("wav:cset.body_len"), Some(&"4".to_string()));
5453 assert!(!md.contains_key("wav:cset.code_page"));
5454 assert!(!md.contains_key("wav:cset.country"));
5455 assert!(!md.contains_key("wav:cset.language"));
5456 assert!(!md.contains_key("wav:cset.dialect"));
5457 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
5458 }
5459
5460 #[test]
5465 fn cset_oversized_body_tolerates_trailing_bytes() {
5466 let mut body = cset_body(932, 81, 17, 1); body.extend_from_slice(&[0xFE, 0xCA, 0xAD, 0xDE]);
5470 assert_eq!(body.len(), 12);
5471 let bytes = wav_with_cset(&body);
5472 let dmx = open_demux_from_bytes(bytes);
5473 let md: std::collections::HashMap<String, String> =
5474 dmx.metadata().iter().cloned().collect();
5475 assert_eq!(md.get("wav:cset.body_len"), Some(&"12".to_string()));
5476 assert_eq!(md.get("wav:cset.code_page"), Some(&"932".to_string()));
5477 assert_eq!(md.get("wav:cset.country"), Some(&"81".to_string()));
5478 assert_eq!(md.get("wav:cset.country_name"), Some(&"Japan".to_string()));
5479 assert_eq!(md.get("wav:cset.language"), Some(&"17".to_string()));
5480 assert_eq!(md.get("wav:cset.dialect"), Some(&"1".to_string()));
5481 assert_eq!(
5482 md.get("wav:cset.language_name"),
5483 Some(&"Japanese".to_string())
5484 );
5485 }
5486
5487 #[test]
5492 fn cset_coexists_with_list_info() {
5493 let mut buf = Vec::new();
5496 buf.extend_from_slice(b"RIFF");
5497 buf.extend_from_slice(&0u32.to_le_bytes());
5498 buf.extend_from_slice(b"WAVE");
5499 buf.extend_from_slice(b"fmt ");
5500 buf.extend_from_slice(&16u32.to_le_bytes());
5501 buf.extend_from_slice(&FMT_PCM.to_le_bytes());
5502 buf.extend_from_slice(&1u16.to_le_bytes());
5503 buf.extend_from_slice(&8_000u32.to_le_bytes());
5504 buf.extend_from_slice(&16_000u32.to_le_bytes());
5505 buf.extend_from_slice(&2u16.to_le_bytes());
5506 buf.extend_from_slice(&16u16.to_le_bytes());
5507 let cset = cset_body(1252, 33, 12, 1);
5509 buf.extend_from_slice(b"CSET");
5510 buf.extend_from_slice(&(cset.len() as u32).to_le_bytes());
5511 buf.extend_from_slice(&cset);
5512 let mut list_body = Vec::new();
5516 list_body.extend_from_slice(b"INFO");
5517 list_body.extend_from_slice(b"INAM");
5518 list_body.extend_from_slice(&1u32.to_le_bytes());
5519 list_body.extend_from_slice(b"T");
5520 list_body.push(0); buf.extend_from_slice(b"LIST");
5522 buf.extend_from_slice(&(list_body.len() as u32).to_le_bytes());
5523 buf.extend_from_slice(&list_body);
5524 buf.extend_from_slice(b"data");
5526 buf.extend_from_slice(&0u32.to_le_bytes());
5527
5528 let dmx = open_demux_from_bytes(buf);
5529 let md: std::collections::HashMap<String, String> =
5530 dmx.metadata().iter().cloned().collect();
5531 assert_eq!(md.get("wav:cset.code_page"), Some(&"1252".to_string()));
5532 assert_eq!(md.get("wav:cset.country_name"), Some(&"France".to_string()));
5533 assert_eq!(
5534 md.get("wav:cset.language_name"),
5535 Some(&"French".to_string())
5536 );
5537 assert_eq!(md.get("title"), Some(&"T".to_string()));
5538 }
5539
5540 #[test]
5544 fn cset_odd_body_padding() {
5545 let mut body = cset_body(1252, 1, 9, 1);
5547 body.push(0xAA);
5548 assert_eq!(body.len() % 2, 1);
5549 let bytes = wav_with_cset(&body);
5550 let dmx = open_demux_from_bytes(bytes);
5551 let md: std::collections::HashMap<String, String> =
5552 dmx.metadata().iter().cloned().collect();
5553 assert_eq!(md.get("wav:cset.body_len"), Some(&"9".to_string()));
5554 assert_eq!(md.get("wav:cset.code_page"), Some(&"1252".to_string()));
5555 assert_eq!(md.get("wav:cset.country_name"), Some(&"USA".to_string()));
5556 assert_eq!(
5557 md.get("wav:cset.language_name"),
5558 Some(&"US English".to_string())
5559 );
5560 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
5561 }
5562
5563 #[test]
5568 fn cset_country_name_table_spot_checks() {
5569 assert_eq!(cset_country_name(1), Some("USA"));
5570 assert_eq!(cset_country_name(44), Some("United Kingdom"));
5571 assert_eq!(cset_country_name(351), Some("Portugal"));
5572 assert_eq!(cset_country_name(358), Some("Finland"));
5573 assert_eq!(cset_country_name(0), Some("None"));
5574 assert_eq!(cset_country_name(500), None);
5575 }
5576
5577 #[test]
5582 fn cset_language_name_table_spot_checks() {
5583 assert_eq!(cset_language_name(9, 1), Some("US English"));
5584 assert_eq!(cset_language_name(9, 2), Some("UK English"));
5585 assert_eq!(cset_language_name(12, 2), Some("Belgian French"));
5586 assert_eq!(cset_language_name(12, 3), Some("Canadian French"));
5587 assert_eq!(cset_language_name(12, 4), Some("Swiss French"));
5588 assert_eq!(cset_language_name(26, 1), Some("Serbo-Croatian (Latin)"));
5589 assert_eq!(cset_language_name(26, 2), Some("Serbo-Croatian (Cyrillic)"));
5590 assert_eq!(cset_language_name(0, 0), Some("None"));
5591 assert_eq!(cset_language_name(0, 1), Some("None"));
5592 assert_eq!(cset_language_name(9, 9), None);
5595 }
5596
5597 fn wav_with_junk(junk_sizes: &[usize]) -> Vec<u8> {
5603 let mut buf = Vec::new();
5604 buf.extend_from_slice(b"RIFF");
5605 buf.extend_from_slice(&0u32.to_le_bytes());
5606 buf.extend_from_slice(b"WAVE");
5607 buf.extend_from_slice(b"fmt ");
5609 buf.extend_from_slice(&16u32.to_le_bytes());
5610 buf.extend_from_slice(&FMT_PCM.to_le_bytes());
5611 buf.extend_from_slice(&1u16.to_le_bytes());
5612 buf.extend_from_slice(&8_000u32.to_le_bytes());
5613 buf.extend_from_slice(&16_000u32.to_le_bytes());
5614 buf.extend_from_slice(&2u16.to_le_bytes());
5615 buf.extend_from_slice(&16u16.to_le_bytes());
5616 for (i, &sz) in junk_sizes.iter().enumerate() {
5617 buf.extend_from_slice(b"JUNK");
5618 buf.extend_from_slice(&(sz as u32).to_le_bytes());
5619 buf.extend(std::iter::repeat_n(i as u8, sz));
5623 if sz % 2 == 1 {
5624 buf.push(0);
5625 }
5626 }
5627 buf.extend_from_slice(b"data");
5629 buf.extend_from_slice(&0u32.to_le_bytes());
5630 buf
5631 }
5632
5633 #[test]
5639 fn junk_single_chunk_surfaces_accounting_metadata() {
5640 let bytes = wav_with_junk(&[16]);
5641 let dmx = open_demux_from_bytes(bytes);
5642 let md: std::collections::HashMap<String, String> =
5643 dmx.metadata().iter().cloned().collect();
5644 assert_eq!(md.get("wav:junk.count"), Some(&"1".to_string()));
5645 assert_eq!(md.get("wav:junk.total_bytes"), Some(&"16".to_string()));
5646 assert_eq!(md.get("wav:junk.0.body_len"), Some(&"16".to_string()));
5647 assert!(
5649 !md.keys()
5650 .any(|k| k.starts_with("wav:junk") && k.ends_with(".body")),
5651 "JUNK chunk body must not be surfaced (Microsoft RIFF MCI §2)"
5652 );
5653 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
5655 }
5656
5657 #[test]
5663 fn junk_multiple_chunks_accumulate() {
5664 let bytes = wav_with_junk(&[32, 8, 100]);
5665 let dmx = open_demux_from_bytes(bytes);
5666 let md: std::collections::HashMap<String, String> =
5667 dmx.metadata().iter().cloned().collect();
5668 assert_eq!(md.get("wav:junk.count"), Some(&"3".to_string()));
5669 assert_eq!(
5670 md.get("wav:junk.total_bytes"),
5671 Some(&(32u64 + 8 + 100).to_string())
5672 );
5673 assert_eq!(md.get("wav:junk.0.body_len"), Some(&"32".to_string()));
5674 assert_eq!(md.get("wav:junk.1.body_len"), Some(&"8".to_string()));
5675 assert_eq!(md.get("wav:junk.2.body_len"), Some(&"100".to_string()));
5676 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
5677 }
5678
5679 #[test]
5685 fn junk_empty_body_still_counts() {
5686 let bytes = wav_with_junk(&[0]);
5687 let dmx = open_demux_from_bytes(bytes);
5688 let md: std::collections::HashMap<String, String> =
5689 dmx.metadata().iter().cloned().collect();
5690 assert_eq!(md.get("wav:junk.count"), Some(&"1".to_string()));
5691 assert_eq!(md.get("wav:junk.total_bytes"), Some(&"0".to_string()));
5692 assert_eq!(md.get("wav:junk.0.body_len"), Some(&"0".to_string()));
5693 }
5694
5695 #[test]
5701 fn junk_absent_emits_no_keys() {
5702 let bytes = wav_with_junk(&[]);
5703 let dmx = open_demux_from_bytes(bytes);
5704 let md: std::collections::HashMap<String, String> =
5705 dmx.metadata().iter().cloned().collect();
5706 assert!(
5707 !md.keys().any(|k| k.starts_with("wav:junk")),
5708 "no JUNK chunk → no wav:junk.* metadata"
5709 );
5710 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
5711 }
5712
5713 #[test]
5720 fn junk_odd_body_padding() {
5721 let bytes = wav_with_junk(&[7]); let dmx = open_demux_from_bytes(bytes);
5723 let md: std::collections::HashMap<String, String> =
5724 dmx.metadata().iter().cloned().collect();
5725 assert_eq!(md.get("wav:junk.0.body_len"), Some(&"7".to_string()));
5726 assert_eq!(md.get("wav:junk.total_bytes"), Some(&"7".to_string()));
5727 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
5729 }
5730
5731 #[test]
5737 fn junk_coexists_with_other_chunks() {
5738 let mut buf = Vec::new();
5739 buf.extend_from_slice(b"RIFF");
5740 buf.extend_from_slice(&0u32.to_le_bytes());
5741 buf.extend_from_slice(b"WAVE");
5742 buf.extend_from_slice(b"fmt ");
5743 buf.extend_from_slice(&16u32.to_le_bytes());
5744 buf.extend_from_slice(&FMT_PCM.to_le_bytes());
5745 buf.extend_from_slice(&1u16.to_le_bytes());
5746 buf.extend_from_slice(&8_000u32.to_le_bytes());
5747 buf.extend_from_slice(&16_000u32.to_le_bytes());
5748 buf.extend_from_slice(&2u16.to_le_bytes());
5749 buf.extend_from_slice(&16u16.to_le_bytes());
5750 buf.extend_from_slice(b"JUNK");
5752 buf.extend_from_slice(&12u32.to_le_bytes());
5753 buf.extend(std::iter::repeat_n(0xAAu8, 12));
5754 let cset = cset_body(1252, 1, 9, 1);
5756 buf.extend_from_slice(b"CSET");
5757 buf.extend_from_slice(&(cset.len() as u32).to_le_bytes());
5758 buf.extend_from_slice(&cset);
5759 let mut list_body = Vec::new();
5761 list_body.extend_from_slice(b"INFO");
5762 list_body.extend_from_slice(b"INAM");
5763 list_body.extend_from_slice(&1u32.to_le_bytes());
5764 list_body.extend_from_slice(b"T");
5765 list_body.push(0);
5766 buf.extend_from_slice(b"LIST");
5767 buf.extend_from_slice(&(list_body.len() as u32).to_le_bytes());
5768 buf.extend_from_slice(&list_body);
5769 buf.extend_from_slice(b"JUNK");
5771 buf.extend_from_slice(&4u32.to_le_bytes());
5772 buf.extend(std::iter::repeat_n(0xBBu8, 4));
5773 buf.extend_from_slice(b"data");
5775 buf.extend_from_slice(&0u32.to_le_bytes());
5776
5777 let dmx = open_demux_from_bytes(buf);
5778 let md: std::collections::HashMap<String, String> =
5779 dmx.metadata().iter().cloned().collect();
5780 assert_eq!(md.get("wav:junk.count"), Some(&"2".to_string()));
5782 assert_eq!(md.get("wav:junk.total_bytes"), Some(&"16".to_string()));
5783 assert_eq!(md.get("wav:junk.0.body_len"), Some(&"12".to_string()));
5784 assert_eq!(md.get("wav:junk.1.body_len"), Some(&"4".to_string()));
5785 assert_eq!(md.get("wav:cset.code_page"), Some(&"1252".to_string()));
5787 assert_eq!(md.get("title"), Some(&"T".to_string()));
5788 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
5789 }
5790
5791 fn wav_with_slnt(slnt_bodies: &[&[u8]]) -> Vec<u8> {
5798 let mut buf = Vec::new();
5799 buf.extend_from_slice(b"RIFF");
5800 buf.extend_from_slice(&0u32.to_le_bytes());
5801 buf.extend_from_slice(b"WAVE");
5802 buf.extend_from_slice(b"fmt ");
5803 buf.extend_from_slice(&16u32.to_le_bytes());
5804 buf.extend_from_slice(&FMT_PCM.to_le_bytes());
5805 buf.extend_from_slice(&1u16.to_le_bytes());
5806 buf.extend_from_slice(&8_000u32.to_le_bytes());
5807 buf.extend_from_slice(&16_000u32.to_le_bytes());
5808 buf.extend_from_slice(&2u16.to_le_bytes());
5809 buf.extend_from_slice(&16u16.to_le_bytes());
5810 for body in slnt_bodies {
5811 buf.extend_from_slice(b"slnt");
5812 buf.extend_from_slice(&(body.len() as u32).to_le_bytes());
5813 buf.extend_from_slice(body);
5814 if body.len() % 2 == 1 {
5815 buf.push(0);
5816 }
5817 }
5818 buf.extend_from_slice(b"data");
5819 buf.extend_from_slice(&0u32.to_le_bytes());
5820 buf
5821 }
5822
5823 #[test]
5829 fn slnt_single_chunk_surfaces_sample_count() {
5830 let bytes = wav_with_slnt(&[&1_000u32.to_le_bytes()]);
5831 let dmx = open_demux_from_bytes(bytes);
5832 let md: std::collections::HashMap<String, String> =
5833 dmx.metadata().iter().cloned().collect();
5834 assert_eq!(md.get("wav:slnt.count"), Some(&"1".to_string()));
5835 assert_eq!(md.get("wav:slnt.total_samples"), Some(&"1000".to_string()));
5836 assert_eq!(md.get("wav:slnt.0.samples"), Some(&"1000".to_string()));
5837 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
5838 }
5839
5840 #[test]
5846 fn slnt_multiple_chunks_accumulate() {
5847 let bytes = wav_with_slnt(&[
5848 &500u32.to_le_bytes(),
5849 &250u32.to_le_bytes(),
5850 &44_100u32.to_le_bytes(),
5851 ]);
5852 let dmx = open_demux_from_bytes(bytes);
5853 let md: std::collections::HashMap<String, String> =
5854 dmx.metadata().iter().cloned().collect();
5855 assert_eq!(md.get("wav:slnt.count"), Some(&"3".to_string()));
5856 assert_eq!(
5857 md.get("wav:slnt.total_samples"),
5858 Some(&(500u64 + 250 + 44_100).to_string())
5859 );
5860 assert_eq!(md.get("wav:slnt.0.samples"), Some(&"500".to_string()));
5861 assert_eq!(md.get("wav:slnt.1.samples"), Some(&"250".to_string()));
5862 assert_eq!(md.get("wav:slnt.2.samples"), Some(&"44100".to_string()));
5863 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
5864 }
5865
5866 #[test]
5871 fn slnt_zero_samples_still_counts() {
5872 let bytes = wav_with_slnt(&[&0u32.to_le_bytes()]);
5873 let dmx = open_demux_from_bytes(bytes);
5874 let md: std::collections::HashMap<String, String> =
5875 dmx.metadata().iter().cloned().collect();
5876 assert_eq!(md.get("wav:slnt.count"), Some(&"1".to_string()));
5877 assert_eq!(md.get("wav:slnt.total_samples"), Some(&"0".to_string()));
5878 assert_eq!(md.get("wav:slnt.0.samples"), Some(&"0".to_string()));
5879 }
5880
5881 #[test]
5886 fn slnt_absent_emits_no_keys() {
5887 let bytes = wav_with_slnt(&[]);
5888 let dmx = open_demux_from_bytes(bytes);
5889 let md: std::collections::HashMap<String, String> =
5890 dmx.metadata().iter().cloned().collect();
5891 assert!(
5892 !md.keys().any(|k| k.starts_with("wav:slnt")),
5893 "no slnt chunk → no wav:slnt.* metadata"
5894 );
5895 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
5896 }
5897
5898 #[test]
5904 fn slnt_short_body_is_opaque() {
5905 let bytes = wav_with_slnt(&[&[0x01, 0x02, 0x03]]);
5908 let dmx = open_demux_from_bytes(bytes);
5909 let md: std::collections::HashMap<String, String> =
5910 dmx.metadata().iter().cloned().collect();
5911 assert_eq!(md.get("wav:slnt.count"), Some(&"1".to_string()));
5912 assert_eq!(md.get("wav:slnt.total_samples"), Some(&"0".to_string()));
5913 assert!(
5914 !md.contains_key("wav:slnt.0.samples"),
5915 "under-length slnt body must not surface a samples value"
5916 );
5917 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
5919 }
5920
5921 #[test]
5927 fn slnt_long_body_decodes_leading_dword() {
5928 let mut body = 7u32.to_le_bytes().to_vec();
5930 body.push(0xFF);
5931 let bytes = wav_with_slnt(&[&body]);
5932 let dmx = open_demux_from_bytes(bytes);
5933 let md: std::collections::HashMap<String, String> =
5934 dmx.metadata().iter().cloned().collect();
5935 assert_eq!(md.get("wav:slnt.count"), Some(&"1".to_string()));
5936 assert_eq!(md.get("wav:slnt.0.samples"), Some(&"7".to_string()));
5937 assert_eq!(md.get("wav:slnt.total_samples"), Some(&"7".to_string()));
5938 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
5939 }
5940
5941 #[test]
5947 fn slnt_coexists_with_other_chunks() {
5948 let mut buf = Vec::new();
5949 buf.extend_from_slice(b"RIFF");
5950 buf.extend_from_slice(&0u32.to_le_bytes());
5951 buf.extend_from_slice(b"WAVE");
5952 buf.extend_from_slice(b"fmt ");
5953 buf.extend_from_slice(&16u32.to_le_bytes());
5954 buf.extend_from_slice(&FMT_PCM.to_le_bytes());
5955 buf.extend_from_slice(&1u16.to_le_bytes());
5956 buf.extend_from_slice(&8_000u32.to_le_bytes());
5957 buf.extend_from_slice(&16_000u32.to_le_bytes());
5958 buf.extend_from_slice(&2u16.to_le_bytes());
5959 buf.extend_from_slice(&16u16.to_le_bytes());
5960 buf.extend_from_slice(b"slnt");
5962 buf.extend_from_slice(&4u32.to_le_bytes());
5963 buf.extend_from_slice(&800u32.to_le_bytes());
5964 buf.extend_from_slice(b"JUNK");
5966 buf.extend_from_slice(&6u32.to_le_bytes());
5967 buf.extend(std::iter::repeat_n(0xAAu8, 6));
5968 let mut list_body = Vec::new();
5970 list_body.extend_from_slice(b"INFO");
5971 list_body.extend_from_slice(b"INAM");
5972 list_body.extend_from_slice(&1u32.to_le_bytes());
5973 list_body.extend_from_slice(b"T");
5974 list_body.push(0);
5975 buf.extend_from_slice(b"LIST");
5976 buf.extend_from_slice(&(list_body.len() as u32).to_le_bytes());
5977 buf.extend_from_slice(&list_body);
5978 buf.extend_from_slice(b"slnt");
5980 buf.extend_from_slice(&4u32.to_le_bytes());
5981 buf.extend_from_slice(&200u32.to_le_bytes());
5982 buf.extend_from_slice(b"data");
5984 buf.extend_from_slice(&0u32.to_le_bytes());
5985
5986 let dmx = open_demux_from_bytes(buf);
5987 let md: std::collections::HashMap<String, String> =
5988 dmx.metadata().iter().cloned().collect();
5989 assert_eq!(md.get("wav:slnt.count"), Some(&"2".to_string()));
5990 assert_eq!(md.get("wav:slnt.total_samples"), Some(&"1000".to_string()));
5991 assert_eq!(md.get("wav:slnt.0.samples"), Some(&"800".to_string()));
5992 assert_eq!(md.get("wav:slnt.1.samples"), Some(&"200".to_string()));
5993 assert_eq!(md.get("wav:junk.count"), Some(&"1".to_string()));
5995 assert_eq!(md.get("title"), Some(&"T".to_string()));
5996 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
5997 }
5998
5999 enum WavlSeg<'a> {
6003 Data(&'a [u8]),
6004 Slnt(u32),
6005 }
6006
6007 fn wav_with_wavl(segs: &[WavlSeg], fact_samples: u32) -> Vec<u8> {
6014 let mut wavl = Vec::new();
6016 wavl.extend_from_slice(b"wavl");
6017 for seg in segs {
6018 match seg {
6019 WavlSeg::Data(body) => {
6020 wavl.extend_from_slice(b"data");
6021 wavl.extend_from_slice(&(body.len() as u32).to_le_bytes());
6022 wavl.extend_from_slice(body);
6023 if body.len() % 2 == 1 {
6024 wavl.push(0);
6025 }
6026 }
6027 WavlSeg::Slnt(samples) => {
6028 wavl.extend_from_slice(b"slnt");
6029 wavl.extend_from_slice(&4u32.to_le_bytes());
6030 wavl.extend_from_slice(&samples.to_le_bytes());
6031 }
6032 }
6033 }
6034
6035 let mut buf = Vec::new();
6036 buf.extend_from_slice(b"RIFF");
6037 buf.extend_from_slice(&0u32.to_le_bytes());
6038 buf.extend_from_slice(b"WAVE");
6039 buf.extend_from_slice(b"fmt ");
6040 buf.extend_from_slice(&16u32.to_le_bytes());
6041 buf.extend_from_slice(&FMT_PCM.to_le_bytes());
6042 buf.extend_from_slice(&1u16.to_le_bytes());
6043 buf.extend_from_slice(&8_000u32.to_le_bytes());
6044 buf.extend_from_slice(&16_000u32.to_le_bytes());
6045 buf.extend_from_slice(&2u16.to_le_bytes());
6046 buf.extend_from_slice(&16u16.to_le_bytes());
6047 buf.extend_from_slice(b"fact");
6049 buf.extend_from_slice(&4u32.to_le_bytes());
6050 buf.extend_from_slice(&fact_samples.to_le_bytes());
6051 buf.extend_from_slice(b"LIST");
6053 buf.extend_from_slice(&(wavl.len() as u32).to_le_bytes());
6054 buf.extend_from_slice(&wavl);
6055 buf
6056 }
6057
6058 #[test]
6062 fn wavl_single_data_segment_decodes() {
6063 let pcm: Vec<u8> = (0..40u8).collect();
6064 let bytes = wav_with_wavl(&[WavlSeg::Data(&pcm)], 20);
6065 let mut dmx = open_demux_from_bytes(bytes);
6066 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
6067 let mut out = Vec::new();
6068 loop {
6069 match dmx.next_packet() {
6070 Ok(p) => out.extend_from_slice(&p.data),
6071 Err(Error::Eof) => break,
6072 Err(e) => panic!("demux error: {e}"),
6073 }
6074 }
6075 assert_eq!(out, pcm);
6076 let md: std::collections::HashMap<String, String> =
6077 dmx.metadata().iter().cloned().collect();
6078 assert_eq!(md.get("wav:wavl.segment_count"), Some(&"1".to_string()));
6079 assert_eq!(md.get("wav:wavl.data_count"), Some(&"1".to_string()));
6080 assert_eq!(md.get("wav:wavl.data_bytes"), Some(&"40".to_string()));
6081 assert_eq!(md.get("wav:wavl.0.kind"), Some(&"data".to_string()));
6082 assert_eq!(md.get("wav:wavl.0.length"), Some(&"40".to_string()));
6083 }
6084
6085 #[test]
6092 fn wavl_interleaved_data_silence_surfaces_segments() {
6093 let a: Vec<u8> = (0..8u8).collect();
6094 let b: Vec<u8> = (100..108u8).collect();
6095 let bytes = wav_with_wavl(
6096 &[WavlSeg::Data(&a), WavlSeg::Slnt(500), WavlSeg::Data(&b)],
6097 1000,
6098 );
6099 let mut dmx = open_demux_from_bytes(bytes);
6100 let mut out = Vec::new();
6102 loop {
6103 match dmx.next_packet() {
6104 Ok(p) => out.extend_from_slice(&p.data),
6105 Err(Error::Eof) => break,
6106 Err(e) => panic!("demux error: {e}"),
6107 }
6108 }
6109 assert_eq!(out, a);
6110 let md: std::collections::HashMap<String, String> =
6111 dmx.metadata().iter().cloned().collect();
6112 assert_eq!(md.get("wav:wavl.segment_count"), Some(&"3".to_string()));
6113 assert_eq!(md.get("wav:wavl.data_count"), Some(&"2".to_string()));
6114 assert_eq!(md.get("wav:wavl.data_bytes"), Some(&"16".to_string()));
6115 assert_eq!(md.get("wav:wavl.0.kind"), Some(&"data".to_string()));
6116 assert_eq!(md.get("wav:wavl.1.kind"), Some(&"slnt".to_string()));
6117 assert_eq!(md.get("wav:wavl.2.kind"), Some(&"data".to_string()));
6118 assert_eq!(md.get("wav:slnt.count"), Some(&"1".to_string()));
6120 assert_eq!(md.get("wav:slnt.total_samples"), Some(&"500".to_string()));
6121 assert_eq!(md.get("wav:slnt.0.samples"), Some(&"500".to_string()));
6122 let s = &dmx.streams()[0];
6124 assert_eq!(s.duration, Some(1000));
6125 }
6126
6127 #[test]
6132 fn wavl_silence_only_has_no_waveform() {
6133 let bytes = wav_with_wavl(&[WavlSeg::Slnt(1000), WavlSeg::Slnt(2000)], 3000);
6134 use std::io::Cursor;
6135 let rs: Box<dyn ReadSeek> = Box::new(Cursor::new(bytes));
6136 match open_demuxer(rs, &oxideav_core::NullCodecResolver) {
6137 Ok(_) => panic!("silence-only wavl must be rejected as having no waveform"),
6138 Err(Error::InvalidData(_)) => {}
6139 Err(e) => panic!("expected InvalidData, got {e:?}"),
6140 }
6141 }
6142
6143 #[test]
6147 fn wavl_odd_data_segment_padding() {
6148 let a: Vec<u8> = (0..7u8).collect(); let b: Vec<u8> = (50..54u8).collect();
6150 let bytes = wav_with_wavl(&[WavlSeg::Data(&a), WavlSeg::Data(&b)], 100);
6151 let dmx = open_demux_from_bytes(bytes);
6152 let md: std::collections::HashMap<String, String> =
6153 dmx.metadata().iter().cloned().collect();
6154 assert_eq!(md.get("wav:wavl.segment_count"), Some(&"2".to_string()));
6155 assert_eq!(md.get("wav:wavl.0.length"), Some(&"7".to_string()));
6156 assert_eq!(md.get("wav:wavl.1.kind"), Some(&"data".to_string()));
6157 assert_eq!(md.get("wav:wavl.1.length"), Some(&"4".to_string()));
6158 }
6159
6160 fn find_chunk(buf: &[u8], fourcc: &[u8; 4]) -> Option<usize> {
6169 let mut i = 12usize;
6171 while i + 8 <= buf.len() {
6172 if &buf[i..i + 4] == fourcc {
6173 return Some(i);
6174 }
6175 let sz = u32::from_le_bytes([buf[i + 4], buf[i + 5], buf[i + 6], buf[i + 7]]) as usize;
6176 i += 8 + sz + (sz % 2);
6177 }
6178 None
6179 }
6180
6181 #[test]
6186 fn guid_canonical_text() {
6187 assert_eq!(fmt_guid(&GUID_PCM), "00000001-0000-0010-8000-00AA00389B71");
6188 assert_eq!(
6189 fmt_guid(&GUID_IEEE_FLOAT),
6190 "00000003-0000-0010-8000-00AA00389B71"
6191 );
6192 assert_eq!(fmt_guid(&GUID_ALAW), "00000006-0000-0010-8000-00AA00389B71");
6193 assert_eq!(
6194 fmt_guid(&GUID_MULAW),
6195 "00000007-0000-0010-8000-00AA00389B71"
6196 );
6197 }
6198
6199 fn info_subchunk(id: &[u8; 4], text: &str) -> Vec<u8> {
6204 let mut out = Vec::new();
6205 out.extend_from_slice(id);
6206 let payload_len = text.len() + 1;
6208 out.extend_from_slice(&(payload_len as u32).to_le_bytes());
6209 out.extend_from_slice(text.as_bytes());
6210 out.push(0);
6211 if payload_len % 2 == 1 {
6215 out.push(0);
6216 }
6217 out
6218 }
6219
6220 fn wav_with_info_entries(entries: &[(&[u8; 4], &str)]) -> Vec<u8> {
6224 let mut list_body = Vec::new();
6225 list_body.extend_from_slice(b"INFO");
6226 for (id, text) in entries {
6227 list_body.extend_from_slice(&info_subchunk(id, text));
6228 }
6229 let mut buf = Vec::new();
6230 buf.extend_from_slice(b"RIFF");
6231 buf.extend_from_slice(&0u32.to_le_bytes());
6232 buf.extend_from_slice(b"WAVE");
6233 buf.extend_from_slice(b"fmt ");
6234 buf.extend_from_slice(&16u32.to_le_bytes());
6235 buf.extend_from_slice(&FMT_PCM.to_le_bytes());
6236 buf.extend_from_slice(&1u16.to_le_bytes());
6237 buf.extend_from_slice(&8_000u32.to_le_bytes());
6238 buf.extend_from_slice(&16_000u32.to_le_bytes());
6239 buf.extend_from_slice(&2u16.to_le_bytes());
6240 buf.extend_from_slice(&16u16.to_le_bytes());
6241 buf.extend_from_slice(b"LIST");
6242 buf.extend_from_slice(&(list_body.len() as u32).to_le_bytes());
6243 buf.extend_from_slice(&list_body);
6244 buf.extend_from_slice(b"data");
6245 buf.extend_from_slice(&0u32.to_le_bytes());
6246 buf
6247 }
6248
6249 #[test]
6254 fn info_list_full_baseline_round_trip() {
6255 let entries: &[(&[u8; 4], &str)] = &[
6258 (b"IARL", "Library of Congress"),
6259 (b"IART", "Michaelangelo"),
6260 (b"ICMS", "Pope Julian II"),
6261 (b"ICMT", "General comment."),
6262 (b"ICOP", "Copyright Encyclopedia International 1991"),
6263 (b"ICRD", "1553-05-03"),
6264 (b"ICRP", "lower right corner"),
6265 (b"IDIM", "8.5 in h, 11 in w"),
6266 (b"IDPI", "300"),
6267 (b"IENG", "Smith, John; Adams, Joe"),
6268 (b"IGNR", "landscape"),
6269 (b"IKEY", "Seattle; aerial view; scenery"),
6270 (b"ILGT", "+10"),
6271 (b"IMED", "lithograph"),
6272 (b"INAM", "Seattle From Above"),
6273 (b"IPLT", "256"),
6274 (b"IPRD", "Encyclopedia of Pacific Northwest Geography"),
6275 (b"ISBJ", "Aerial view of Seattle"),
6276 (b"ISFT", "Microsoft WaveEdit"),
6277 (b"ISHP", "+5"),
6278 (b"ISRC", "Trey Research"),
6279 (b"ISRF", "slide"),
6280 (b"ITCH", "Smith, John"),
6281 ];
6282
6283 let bytes = wav_with_info_entries(entries);
6284 let dmx = open_demux_from_bytes(bytes);
6285 let md: std::collections::HashMap<String, String> =
6286 dmx.metadata().iter().cloned().collect();
6287
6288 let expected: &[(&str, &str)] = &[
6291 ("archival_location", "Library of Congress"),
6292 ("artist", "Michaelangelo"),
6293 ("commissioned", "Pope Julian II"),
6294 ("comment", "General comment."),
6295 ("copyright", "Copyright Encyclopedia International 1991"),
6296 ("date", "1553-05-03"),
6297 ("cropped", "lower right corner"),
6298 ("dimensions", "8.5 in h, 11 in w"),
6299 ("dpi", "300"),
6300 ("engineer", "Smith, John; Adams, Joe"),
6301 ("genre", "landscape"),
6302 ("keywords", "Seattle; aerial view; scenery"),
6303 ("lightness", "+10"),
6304 ("medium", "lithograph"),
6305 ("title", "Seattle From Above"),
6306 ("palette_setting", "256"),
6307 ("album", "Encyclopedia of Pacific Northwest Geography"),
6308 ("subject", "Aerial view of Seattle"),
6309 ("encoder", "Microsoft WaveEdit"),
6310 ("sharpness", "+5"),
6311 ("source", "Trey Research"),
6312 ("source_form", "slide"),
6313 ("technician", "Smith, John"),
6314 ];
6315 for (key, value) in expected {
6316 assert_eq!(
6317 md.get(*key),
6318 Some(&(*value).to_string()),
6319 "INFO key {key:?} missing or wrong; got {:?}",
6320 md.get(*key)
6321 );
6322 }
6323 }
6324
6325 #[test]
6331 fn info_id_to_key_legacy_aliases_preserved() {
6332 assert_eq!(info_id_to_key(b"INAM"), Some("title"));
6333 assert_eq!(info_id_to_key(b"IART"), Some("artist"));
6334 assert_eq!(info_id_to_key(b"IPRD"), Some("album"));
6335 assert_eq!(info_id_to_key(b"ICMT"), Some("comment"));
6336 assert_eq!(info_id_to_key(b"ICRD"), Some("date"));
6337 assert_eq!(info_id_to_key(b"IGNR"), Some("genre"));
6338 assert_eq!(info_id_to_key(b"ICOP"), Some("copyright"));
6339 assert_eq!(info_id_to_key(b"IENG"), Some("engineer"));
6340 assert_eq!(info_id_to_key(b"ITCH"), Some("technician"));
6341 assert_eq!(info_id_to_key(b"ISFT"), Some("encoder"));
6342 assert_eq!(info_id_to_key(b"ISBJ"), Some("subject"));
6343 assert_eq!(info_id_to_key(b"ITRK"), Some("track"));
6344 }
6345
6346 #[test]
6352 fn info_id_to_key_baseline_completeness() {
6353 const BASELINE: &[&[u8; 4]] = &[
6354 b"IARL", b"IART", b"ICMS", b"ICMT", b"ICOP", b"ICRD", b"ICRP", b"IDIM", b"IDPI",
6355 b"IENG", b"IGNR", b"IKEY", b"ILGT", b"IMED", b"INAM", b"IPLT", b"IPRD", b"ISBJ",
6356 b"ISFT", b"ISHP", b"ISRC", b"ISRF", b"ITCH",
6357 ];
6358 for id in BASELINE {
6359 assert!(
6360 info_id_to_key(id).is_some(),
6361 "baseline INFO sub-ID {:?} missing from info_id_to_key",
6362 std::str::from_utf8(*id).unwrap()
6363 );
6364 }
6365 assert_eq!(info_id_to_key(b"ZZZZ"), None);
6367 assert_eq!(info_id_to_key(b"IRAL"), None);
6369 }
6370
6371 #[test]
6376 fn info_list_unknown_subchunk_is_skipped() {
6377 let entries: &[(&[u8; 4], &str)] = &[
6378 (b"INAM", "Title"),
6379 (b"IZZZ", "ignored"),
6382 (b"IART", "Artist"),
6383 ];
6384 let bytes = wav_with_info_entries(entries);
6385 let dmx = open_demux_from_bytes(bytes);
6386 let md: std::collections::HashMap<String, String> =
6387 dmx.metadata().iter().cloned().collect();
6388 assert_eq!(md.get("title"), Some(&"Title".to_string()));
6389 assert_eq!(md.get("artist"), Some(&"Artist".to_string()));
6390 assert!(md
6392 .keys()
6393 .all(|k| !k.contains("IZZZ") && !k.contains("izzz")));
6394 }
6395
6396 #[allow(clippy::too_many_arguments)]
6406 fn synth_rf64(
6407 magic: &[u8; 4],
6408 ds64_data_size: u64,
6409 ds64_sample_count: u64,
6410 extra_table: &[([u8; 4], u64)],
6411 data_payload: &[u8],
6412 channels: u16,
6413 sample_rate: u32,
6414 bits_per_sample: u16,
6415 ) -> Vec<u8> {
6416 let mut ds64_body: Vec<u8> = Vec::new();
6418 ds64_body.extend_from_slice(&0u64.to_le_bytes());
6421 ds64_body.extend_from_slice(&ds64_data_size.to_le_bytes());
6422 ds64_body.extend_from_slice(&ds64_sample_count.to_le_bytes());
6423 ds64_body.extend_from_slice(&(extra_table.len() as u32).to_le_bytes());
6424 for (id, sz) in extra_table {
6425 ds64_body.extend_from_slice(id);
6426 ds64_body.extend_from_slice(&sz.to_le_bytes());
6427 }
6428
6429 let block_align = (bits_per_sample / 8) * channels;
6432 let byte_rate = sample_rate * block_align as u32;
6433 let mut fmt_body: Vec<u8> = Vec::new();
6434 fmt_body.extend_from_slice(&WAVE_FORMAT_PCM.to_le_bytes());
6435 fmt_body.extend_from_slice(&channels.to_le_bytes());
6436 fmt_body.extend_from_slice(&sample_rate.to_le_bytes());
6437 fmt_body.extend_from_slice(&byte_rate.to_le_bytes());
6438 fmt_body.extend_from_slice(&block_align.to_le_bytes());
6439 fmt_body.extend_from_slice(&bits_per_sample.to_le_bytes());
6440
6441 let fact_body = SIZE64_SENTINEL.to_le_bytes();
6444
6445 let mut out: Vec<u8> = Vec::new();
6448 out.extend_from_slice(magic);
6449 out.extend_from_slice(&SIZE64_SENTINEL.to_le_bytes());
6450 out.extend_from_slice(b"WAVE");
6451
6452 out.extend_from_slice(b"ds64");
6453 out.extend_from_slice(&(ds64_body.len() as u32).to_le_bytes());
6454 let ds64_riff_field_off = out.len();
6455 out.extend_from_slice(&ds64_body);
6456
6457 out.extend_from_slice(b"fmt ");
6458 out.extend_from_slice(&(fmt_body.len() as u32).to_le_bytes());
6459 out.extend_from_slice(&fmt_body);
6460
6461 out.extend_from_slice(b"fact");
6462 out.extend_from_slice(&(fact_body.len() as u32).to_le_bytes());
6463 out.extend_from_slice(&fact_body);
6464
6465 out.extend_from_slice(b"data");
6466 out.extend_from_slice(&SIZE64_SENTINEL.to_le_bytes());
6467 out.extend_from_slice(data_payload);
6468
6469 let riff_size_64 = (out.len() as u64) - 8;
6473 out[ds64_riff_field_off..ds64_riff_field_off + 8]
6474 .copy_from_slice(&riff_size_64.to_le_bytes());
6475 out
6476 }
6477
6478 #[test]
6484 fn rf64_demux_pcm_s16_with_ds64_overrides() {
6485 let payload: Vec<u8> = (0..200u8).collect();
6488 let bytes = synth_rf64(b"RF64", 200, 100, &[], &payload, 1, 48_000, 16);
6489
6490 let dmx = open_demux_from_bytes(bytes);
6491 let md: std::collections::HashMap<String, String> =
6492 dmx.metadata().iter().cloned().collect();
6493
6494 assert_eq!(md.get("wav:rf64.magic"), Some(&"RF64".to_string()));
6495 assert_eq!(md.get("wav:rf64.data_size"), Some(&"200".to_string()));
6496 assert_eq!(md.get("wav:rf64.sample_count"), Some(&"100".to_string()));
6497 assert_eq!(md.get("wav:rf64.table.count"), Some(&"0".to_string()));
6498
6499 let stream = &dmx.streams()[0];
6502 assert_eq!(stream.duration, Some(100));
6503 assert_eq!(stream.params.codec_id, CodecId::new("pcm_s16le"));
6504 }
6505
6506 #[test]
6510 fn bw64_demux_pcm_s16_treated_as_rf64() {
6511 let payload: Vec<u8> = (0..40u8).collect();
6512 let bytes = synth_rf64(b"BW64", 40, 20, &[], &payload, 1, 48_000, 16);
6513
6514 let dmx = open_demux_from_bytes(bytes);
6515 let md: std::collections::HashMap<String, String> =
6516 dmx.metadata().iter().cloned().collect();
6517 assert_eq!(md.get("wav:rf64.magic"), Some(&"BW64".to_string()));
6518 assert_eq!(md.get("wav:rf64.data_size"), Some(&"40".to_string()));
6519 assert_eq!(md.get("wav:rf64.sample_count"), Some(&"20".to_string()));
6520 assert_eq!(dmx.streams()[0].duration, Some(20));
6521 }
6522
6523 #[test]
6528 fn rf64_table_promotes_non_data_chunk_size() {
6529 let mut list_body: Vec<u8> = Vec::new();
6531 list_body.extend_from_slice(b"INFO");
6532 list_body.extend_from_slice(b"INAM");
6533 list_body.extend_from_slice(&6u32.to_le_bytes()); list_body.extend_from_slice(b"Hello\0");
6535 let list_body_len = list_body.len() as u64;
6536
6537 let mut ds64_body: Vec<u8> = Vec::new();
6541 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");
6546 ds64_body.extend_from_slice(&list_body_len.to_le_bytes());
6547
6548 let payload: Vec<u8> = (0..8u8).collect();
6549 let fmt_body = {
6550 let mut f = Vec::new();
6551 f.extend_from_slice(&WAVE_FORMAT_PCM.to_le_bytes());
6552 f.extend_from_slice(&1u16.to_le_bytes()); f.extend_from_slice(&48_000u32.to_le_bytes());
6554 f.extend_from_slice(&(48_000u32 * 2).to_le_bytes());
6555 f.extend_from_slice(&2u16.to_le_bytes()); f.extend_from_slice(&16u16.to_le_bytes());
6557 f
6558 };
6559
6560 let mut out: Vec<u8> = Vec::new();
6561 out.extend_from_slice(b"RF64");
6562 out.extend_from_slice(&SIZE64_SENTINEL.to_le_bytes());
6563 out.extend_from_slice(b"WAVE");
6564 out.extend_from_slice(b"ds64");
6565 out.extend_from_slice(&(ds64_body.len() as u32).to_le_bytes());
6566 let ds64_riff_off = out.len();
6567 out.extend_from_slice(&ds64_body);
6568 out.extend_from_slice(b"fmt ");
6569 out.extend_from_slice(&(fmt_body.len() as u32).to_le_bytes());
6570 out.extend_from_slice(&fmt_body);
6571 out.extend_from_slice(b"LIST");
6573 out.extend_from_slice(&SIZE64_SENTINEL.to_le_bytes());
6574 out.extend_from_slice(&list_body);
6575 out.extend_from_slice(b"data");
6576 out.extend_from_slice(&SIZE64_SENTINEL.to_le_bytes());
6577 out.extend_from_slice(&payload);
6578 let riff_size_64 = (out.len() as u64) - 8;
6579 out[ds64_riff_off..ds64_riff_off + 8].copy_from_slice(&riff_size_64.to_le_bytes());
6580
6581 let dmx = open_demux_from_bytes(out);
6582 let md: std::collections::HashMap<String, String> =
6583 dmx.metadata().iter().cloned().collect();
6584
6585 assert_eq!(md.get("wav:rf64.table.count"), Some(&"1".to_string()));
6587 assert_eq!(md.get("wav:rf64.table.0.id"), Some(&"LIST".to_string()));
6588 assert_eq!(
6589 md.get("wav:rf64.table.0.size"),
6590 Some(&list_body_len.to_string())
6591 );
6592 assert_eq!(md.get("title"), Some(&"Hello".to_string()));
6596 }
6597
6598 #[test]
6604 fn rf64_sentinel_without_ds64_is_rejected() {
6605 let fmt_body = {
6608 let mut f = Vec::new();
6609 f.extend_from_slice(&WAVE_FORMAT_PCM.to_le_bytes());
6610 f.extend_from_slice(&1u16.to_le_bytes());
6611 f.extend_from_slice(&48_000u32.to_le_bytes());
6612 f.extend_from_slice(&(48_000u32 * 2).to_le_bytes());
6613 f.extend_from_slice(&2u16.to_le_bytes());
6614 f.extend_from_slice(&16u16.to_le_bytes());
6615 f
6616 };
6617 let mut out: Vec<u8> = Vec::new();
6618 out.extend_from_slice(b"RIFF");
6619 out.extend_from_slice(&0u32.to_le_bytes());
6620 out.extend_from_slice(b"WAVE");
6621 out.extend_from_slice(b"fmt ");
6622 out.extend_from_slice(&(fmt_body.len() as u32).to_le_bytes());
6623 out.extend_from_slice(&fmt_body);
6624 out.extend_from_slice(b"data");
6625 out.extend_from_slice(&SIZE64_SENTINEL.to_le_bytes());
6626
6627 use std::io::Cursor;
6628 let rs: Box<dyn ReadSeek> = Box::new(Cursor::new(out));
6629 let res = open_demuxer(rs, &oxideav_core::NullCodecResolver);
6630 assert!(res.is_err(), "sentinel without ds64 must be rejected");
6631 }
6632
6633 #[test]
6638 fn rf64_ds64_short_body_is_rejected() {
6639 let mut out: Vec<u8> = Vec::new();
6640 out.extend_from_slice(b"RF64");
6641 out.extend_from_slice(&SIZE64_SENTINEL.to_le_bytes());
6642 out.extend_from_slice(b"WAVE");
6643 out.extend_from_slice(b"ds64");
6644 out.extend_from_slice(&8u32.to_le_bytes()); out.extend_from_slice(&[0u8; 8]);
6646
6647 use std::io::Cursor;
6648 let rs: Box<dyn ReadSeek> = Box::new(Cursor::new(out));
6649 let res = open_demuxer(rs, &oxideav_core::NullCodecResolver);
6650 assert!(res.is_err(), "ds64 < 28 bytes must be rejected");
6651 }
6652}