1use oxideav_core::{
37 CodecId, CodecParameters, CodecResolver, Error, MediaType, Packet, Result, SampleFormat,
38 StreamInfo, TimeBase,
39};
40use oxideav_core::{ContainerRegistry, Demuxer, Muxer, ReadSeek, WriteSeek};
41use std::io::{Read, Seek, SeekFrom, Write};
42
43pub fn register(reg: &mut ContainerRegistry) {
44 reg.register_demuxer("wav", open_demuxer);
45 reg.register_muxer("wav", open_muxer);
46 reg.register_extension("wav", "wav");
47 reg.register_extension("wave", "wav");
48 reg.register_probe("wav", probe);
49}
50
51fn probe(p: &oxideav_core::ProbeData) -> u8 {
53 if p.buf.len() < 12 {
54 return 0;
55 }
56 if &p.buf[0..4] == b"RIFF" && &p.buf[8..12] == b"WAVE" {
57 100
58 } else {
59 0
60 }
61}
62
63pub const WAVE_FORMAT_PCM: u16 = 0x0001;
68pub const WAVE_FORMAT_IEEE_FLOAT: u16 = 0x0003;
70pub const WAVE_FORMAT_ALAW: u16 = 0x0006;
72pub const WAVE_FORMAT_MULAW: u16 = 0x0007;
74pub const WAVE_FORMAT_EXTENSIBLE: u16 = 0xFFFE;
78
79const FMT_PCM: u16 = WAVE_FORMAT_PCM;
81const FMT_IEEE_FLOAT: u16 = WAVE_FORMAT_IEEE_FLOAT;
82const FMT_ALAW: u16 = WAVE_FORMAT_ALAW;
83const FMT_MULAW: u16 = WAVE_FORMAT_MULAW;
84const FMT_EXTENSIBLE: u16 = WAVE_FORMAT_EXTENSIBLE;
85
86const GUID_PCM: [u8; 16] = [
91 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71,
92];
93const GUID_IEEE_FLOAT: [u8; 16] = [
94 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71,
95];
96const GUID_ALAW: [u8; 16] = [
97 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71,
98];
99const GUID_MULAW: [u8; 16] = [
100 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71,
101];
102
103fn fmt_guid(g: &[u8; 16]) -> String {
108 format!(
109 "{:08X}-{:04X}-{:04X}-{:02X}{:02X}-{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}",
110 u32::from_le_bytes([g[0], g[1], g[2], g[3]]),
111 u16::from_le_bytes([g[4], g[5]]),
112 u16::from_le_bytes([g[6], g[7]]),
113 g[8],
114 g[9],
115 g[10],
116 g[11],
117 g[12],
118 g[13],
119 g[14],
120 g[15],
121 )
122}
123
124fn open_demuxer(
127 mut input: Box<dyn ReadSeek>,
128 _codecs: &dyn CodecResolver,
129) -> Result<Box<dyn Demuxer>> {
130 let mut hdr = [0u8; 12];
131 input.read_exact(&mut hdr)?;
132 if &hdr[0..4] != b"RIFF" || &hdr[8..12] != b"WAVE" {
133 return Err(Error::invalid("not a RIFF/WAVE file"));
134 }
135
136 let mut fmt: Option<WaveFmt> = None;
138 let mut metadata: Vec<(String, String)> = Vec::new();
139 let data_offset: u64;
140 let data_size: u64;
141 loop {
142 let mut chdr = [0u8; 8];
143 input.read_exact(&mut chdr)?;
144 let id = &chdr[0..4];
145 let size = u32::from_le_bytes([chdr[4], chdr[5], chdr[6], chdr[7]]) as u64;
146 match id {
147 b"fmt " => {
148 let mut buf = vec![0u8; size as usize];
149 input.read_exact(&mut buf)?;
150 fmt = Some(parse_fmt(&buf)?);
151 if size % 2 == 1 {
152 input.seek(SeekFrom::Current(1))?;
153 }
154 }
155 b"LIST" => {
156 let mut buf = vec![0u8; size as usize];
157 input.read_exact(&mut buf)?;
158 parse_list_chunk(&buf, &mut metadata);
159 if size % 2 == 1 {
160 input.seek(SeekFrom::Current(1))?;
161 }
162 }
163 b"bext" => {
164 let mut buf = vec![0u8; size as usize];
165 input.read_exact(&mut buf)?;
166 parse_bext_chunk(&buf, &mut metadata);
167 if size % 2 == 1 {
168 input.seek(SeekFrom::Current(1))?;
169 }
170 }
171 b"cue " => {
172 let mut buf = vec![0u8; size as usize];
173 input.read_exact(&mut buf)?;
174 parse_cue_chunk(&buf, &mut metadata);
175 if size % 2 == 1 {
176 input.seek(SeekFrom::Current(1))?;
177 }
178 }
179 b"plst" => {
180 let mut buf = vec![0u8; size as usize];
181 input.read_exact(&mut buf)?;
182 parse_plst_chunk(&buf, &mut metadata);
183 if size % 2 == 1 {
184 input.seek(SeekFrom::Current(1))?;
185 }
186 }
187 b"smpl" => {
188 let mut buf = vec![0u8; size as usize];
189 input.read_exact(&mut buf)?;
190 parse_smpl_chunk(&buf, &mut metadata);
191 if size % 2 == 1 {
192 input.seek(SeekFrom::Current(1))?;
193 }
194 }
195 b"inst" => {
196 let mut buf = vec![0u8; size as usize];
197 input.read_exact(&mut buf)?;
198 parse_inst_chunk(&buf, &mut metadata);
199 if size % 2 == 1 {
200 input.seek(SeekFrom::Current(1))?;
201 }
202 }
203 b"data" => {
204 data_offset = input.stream_position()?;
205 data_size = size;
206 break;
207 }
208 _ => {
209 let pad = size + (size % 2);
210 input.seek(SeekFrom::Current(pad as i64))?;
211 }
212 }
213 }
214 let fmt = fmt.ok_or_else(|| Error::invalid("WAV missing fmt chunk"))?;
215
216 let codec_id = resolve_codec(&fmt)?;
217 let sample_fmt = decoded_sample_format(&codec_id);
225
226 let time_base = TimeBase::new(1, fmt.sample_rate as i64);
227 let block_align = fmt.block_align.max(1) as u64;
228 let total_samples = data_size / block_align;
229 let duration_micros: i64 = if fmt.sample_rate > 0 {
230 (total_samples as i128 * 1_000_000 / fmt.sample_rate as i128) as i64
231 } else {
232 0
233 };
234
235 let mut params = CodecParameters::audio(codec_id);
236 params.tag = Some(oxideav_core::CodecTag::wave_format(fmt.format_tag));
237 params.channels = Some(fmt.channels);
238 params.sample_rate = Some(fmt.sample_rate);
239 params.sample_format = sample_fmt;
240 params.bit_rate = Some(8 * block_align * (fmt.sample_rate as u64));
244
245 if fmt.format_tag == FMT_EXTENSIBLE {
250 if let Some(valid) = fmt.valid_bits_per_sample {
251 metadata.push((
252 "wav:fmt.valid_bits_per_sample".to_string(),
253 valid.to_string(),
254 ));
255 }
256 if let Some(mask) = fmt.channel_mask {
257 metadata.push(("wav:fmt.channel_mask".to_string(), format!("0x{mask:08X}")));
258 }
259 if let Some(sub) = &fmt.subformat {
260 metadata.push(("wav:fmt.subformat".to_string(), fmt_guid(sub)));
261 }
262 }
263
264 let stream = StreamInfo {
265 index: 0,
266 time_base,
267 duration: Some(total_samples as i64),
268 start_time: Some(0),
269 params,
270 };
271
272 Ok(Box::new(WavDemuxer {
273 input,
274 streams: vec![stream],
275 data_offset,
276 data_end: data_offset + data_size,
277 cursor: data_offset,
278 block_align,
279 chunk_frames: 1024,
280 samples_emitted: 0,
281 metadata,
282 duration_micros,
283 format_tag: fmt.format_tag,
284 valid_bits_per_sample: fmt.valid_bits_per_sample,
285 channel_mask: fmt.channel_mask,
286 subformat: fmt.subformat,
287 }))
288}
289
290fn parse_list_chunk(buf: &[u8], out: &mut Vec<(String, String)>) {
297 if buf.len() < 4 {
298 return;
299 }
300 match &buf[0..4] {
301 b"INFO" => parse_info_list(&buf[4..], out),
302 b"adtl" => parse_adtl_list(&buf[4..], out),
303 _ => {}
304 }
305}
306
307fn parse_info_list(buf: &[u8], out: &mut Vec<(String, String)>) {
308 let mut i = 0usize;
309 while i + 8 <= buf.len() {
310 let id: [u8; 4] = [buf[i], buf[i + 1], buf[i + 2], buf[i + 3]];
311 let size = u32::from_le_bytes([buf[i + 4], buf[i + 5], buf[i + 6], buf[i + 7]]) as usize;
312 i += 8;
313 if i + size > buf.len() {
314 break;
315 }
316 let raw = &buf[i..i + size];
317 let end = raw.iter().position(|&b| b == 0).unwrap_or(raw.len());
318 let value = String::from_utf8_lossy(&raw[..end]).trim().to_string();
319 let key = info_id_to_key(&id);
320 if !value.is_empty() {
321 if let Some(k) = key {
322 out.push((k.to_string(), value));
323 }
324 }
325 i += size;
326 if size % 2 == 1 {
327 i += 1;
328 }
329 }
330}
331
332fn parse_adtl_list(buf: &[u8], out: &mut Vec<(String, String)>) {
350 let mut i = 0usize;
351 while i + 8 <= buf.len() {
352 let id: [u8; 4] = [buf[i], buf[i + 1], buf[i + 2], buf[i + 3]];
353 let size = u32::from_le_bytes([buf[i + 4], buf[i + 5], buf[i + 6], buf[i + 7]]) as usize;
354 i += 8;
355 if i + size > buf.len() {
356 break;
357 }
358 let body = &buf[i..i + size];
359 match &id {
360 b"labl" | b"note" if body.len() >= 4 => {
361 let dw_name = u32::from_le_bytes([body[0], body[1], body[2], body[3]]);
362 let raw = &body[4..];
363 let end = raw.iter().position(|&b| b == 0).unwrap_or(raw.len());
364 let text = String::from_utf8_lossy(&raw[..end]).trim().to_string();
365 if !text.is_empty() {
366 let sub = if &id == b"labl" { "labl" } else { "note" };
367 out.push((format!("wav:adtl.{sub}.{dw_name}"), text));
368 }
369 }
370 b"ltxt" if body.len() >= 20 => {
374 let dw_name = u32::from_le_bytes([body[0], body[1], body[2], body[3]]);
375 let dw_length = u32::from_le_bytes([body[4], body[5], body[6], body[7]]);
376 let purpose = &body[8..12];
377 out.push((
378 format!("wav:adtl.ltxt.{dw_name}.length"),
379 dw_length.to_string(),
380 ));
381 let purpose_str = if purpose.iter().all(|&b| (0x20..=0x7E).contains(&b)) {
383 String::from_utf8_lossy(purpose).to_string()
384 } else {
385 format!(
386 "0x{:02X}{:02X}{:02X}{:02X}",
387 purpose[0], purpose[1], purpose[2], purpose[3]
388 )
389 };
390 out.push((format!("wav:adtl.ltxt.{dw_name}.purpose"), purpose_str));
391 let raw = &body[20..];
392 let end = raw.iter().position(|&b| b == 0).unwrap_or(raw.len());
396 let text = String::from_utf8_lossy(&raw[..end]).trim().to_string();
397 if !text.is_empty() {
398 out.push((format!("wav:adtl.ltxt.{dw_name}.text"), text));
399 }
400 }
401 _ => {}
406 }
407 i += size;
408 if size % 2 == 1 {
409 i += 1;
410 }
411 }
412}
413
414fn parse_cue_chunk(buf: &[u8], out: &mut Vec<(String, String)>) {
437 if buf.len() < 4 {
438 return;
439 }
440 let count_claimed = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
441 let body = &buf[4..];
442 const REC_LEN: usize = 24;
443 let count_actual = (body.len() / REC_LEN) as u32;
444 let count = count_claimed.min(count_actual);
445 out.push(("wav:cue.count".to_string(), count.to_string()));
446 for i in 0..count as usize {
447 let off = i * REC_LEN;
448 let dw_name = u32::from_le_bytes([body[off], body[off + 1], body[off + 2], body[off + 3]]);
449 let dw_position =
450 u32::from_le_bytes([body[off + 4], body[off + 5], body[off + 6], body[off + 7]]);
451 let fcc_chunk = &body[off + 8..off + 12];
452 let dw_chunk_start = u32::from_le_bytes([
453 body[off + 12],
454 body[off + 13],
455 body[off + 14],
456 body[off + 15],
457 ]);
458 let dw_block_start = u32::from_le_bytes([
459 body[off + 16],
460 body[off + 17],
461 body[off + 18],
462 body[off + 19],
463 ]);
464 let dw_sample_offset = u32::from_le_bytes([
465 body[off + 20],
466 body[off + 21],
467 body[off + 22],
468 body[off + 23],
469 ]);
470 let fcc_str = if fcc_chunk.iter().all(|&b| (0x20..=0x7E).contains(&b)) {
471 String::from_utf8_lossy(fcc_chunk).to_string()
472 } else {
473 format!(
474 "0x{:02X}{:02X}{:02X}{:02X}",
475 fcc_chunk[0], fcc_chunk[1], fcc_chunk[2], fcc_chunk[3]
476 )
477 };
478 out.push((
479 format!("wav:cue.{dw_name}.position"),
480 dw_position.to_string(),
481 ));
482 out.push((format!("wav:cue.{dw_name}.fcc_chunk"), fcc_str));
483 out.push((
484 format!("wav:cue.{dw_name}.chunk_start"),
485 dw_chunk_start.to_string(),
486 ));
487 out.push((
488 format!("wav:cue.{dw_name}.block_start"),
489 dw_block_start.to_string(),
490 ));
491 out.push((
492 format!("wav:cue.{dw_name}.sample_offset"),
493 dw_sample_offset.to_string(),
494 ));
495 }
496}
497
498fn parse_plst_chunk(buf: &[u8], out: &mut Vec<(String, String)>) {
521 if buf.len() < 4 {
522 return;
523 }
524 let count_claimed = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
525 let body = &buf[4..];
526 const REC_LEN: usize = 12;
527 let count_actual = (body.len() / REC_LEN) as u32;
528 let count = count_claimed.min(count_actual);
529 out.push(("wav:plst.count".to_string(), count.to_string()));
530 for i in 0..count as usize {
531 let off = i * REC_LEN;
532 let dw_name = u32::from_le_bytes([body[off], body[off + 1], body[off + 2], body[off + 3]]);
533 let dw_length =
534 u32::from_le_bytes([body[off + 4], body[off + 5], body[off + 6], body[off + 7]]);
535 let dw_loops =
536 u32::from_le_bytes([body[off + 8], body[off + 9], body[off + 10], body[off + 11]]);
537 out.push((format!("wav:plst.{i}.cue_id"), dw_name.to_string()));
538 out.push((format!("wav:plst.{i}.length"), dw_length.to_string()));
539 out.push((format!("wav:plst.{i}.loops"), dw_loops.to_string()));
540 }
541}
542
543fn parse_smpl_chunk(buf: &[u8], out: &mut Vec<(String, String)>) {
579 const FIXED_LEN: usize = 36;
580 const LOOP_LEN: usize = 24;
581 if buf.len() < FIXED_LEN {
582 return;
583 }
584 let r = |off: usize| -> u32 {
585 u32::from_le_bytes([buf[off], buf[off + 1], buf[off + 2], buf[off + 3]])
586 };
587 let manufacturer = r(0);
588 let product = r(4);
589 let sample_period = r(8);
590 let midi_unity_note = r(12);
591 let midi_pitch_fraction = r(16);
592 let smpte_format = r(20);
593 let smpte_offset = r(24);
594 let num_loops_claimed = r(28);
595 let sampler_data_len = r(32);
596
597 out.push((
598 "wav:smpl.manufacturer".to_string(),
599 manufacturer.to_string(),
600 ));
601 out.push(("wav:smpl.product".to_string(), product.to_string()));
602 out.push((
603 "wav:smpl.sample_period".to_string(),
604 sample_period.to_string(),
605 ));
606 out.push((
607 "wav:smpl.midi_unity_note".to_string(),
608 midi_unity_note.to_string(),
609 ));
610 out.push((
611 "wav:smpl.midi_pitch_fraction".to_string(),
612 midi_pitch_fraction.to_string(),
613 ));
614 out.push((
615 "wav:smpl.smpte_format".to_string(),
616 smpte_format.to_string(),
617 ));
618 let smpte_hh = (smpte_offset >> 24) & 0xFF;
622 let smpte_mm = (smpte_offset >> 16) & 0xFF;
623 let smpte_ss = (smpte_offset >> 8) & 0xFF;
624 let smpte_ff = smpte_offset & 0xFF;
625 out.push((
626 "wav:smpl.smpte_offset".to_string(),
627 format!("{smpte_hh:02}:{smpte_mm:02}:{smpte_ss:02}:{smpte_ff:02}"),
628 ));
629 out.push((
630 "wav:smpl.sampler_data_len".to_string(),
631 sampler_data_len.to_string(),
632 ));
633
634 let body = &buf[FIXED_LEN..];
635 let num_loops_fits = (body.len() / LOOP_LEN) as u32;
636 let num_loops = num_loops_claimed.min(num_loops_fits);
637 out.push((
638 "wav:smpl.num_sample_loops".to_string(),
639 num_loops.to_string(),
640 ));
641 for i in 0..num_loops as usize {
642 let off = i * LOOP_LEN;
643 let loop_field = |word: usize| -> u32 {
644 let p = off + word * 4;
645 u32::from_le_bytes([body[p], body[p + 1], body[p + 2], body[p + 3]])
646 };
647 let cue_point_id = loop_field(0);
648 let loop_type = loop_field(1);
649 let start = loop_field(2);
650 let end = loop_field(3);
651 let fraction = loop_field(4);
652 let play_count = loop_field(5);
653 out.push((
654 format!("wav:smpl.loop.{i}.cue_point_id"),
655 cue_point_id.to_string(),
656 ));
657 out.push((format!("wav:smpl.loop.{i}.type"), loop_type.to_string()));
658 out.push((format!("wav:smpl.loop.{i}.start"), start.to_string()));
659 out.push((format!("wav:smpl.loop.{i}.end"), end.to_string()));
660 out.push((format!("wav:smpl.loop.{i}.fraction"), fraction.to_string()));
661 out.push((
662 format!("wav:smpl.loop.{i}.play_count"),
663 play_count.to_string(),
664 ));
665 }
666}
667
668fn parse_inst_chunk(buf: &[u8], out: &mut Vec<(String, String)>) {
686 const FIXED_LEN: usize = 7;
687 if buf.len() < FIXED_LEN {
688 return;
689 }
690 let unshifted_note = buf[0];
691 let fine_tune = buf[1] as i8;
692 let gain = buf[2] as i8;
693 let low_note = buf[3];
694 let high_note = buf[4];
695 let low_velocity = buf[5];
696 let high_velocity = buf[6];
697 out.push((
698 "wav:inst.unshifted_note".to_string(),
699 unshifted_note.to_string(),
700 ));
701 out.push(("wav:inst.fine_tune".to_string(), fine_tune.to_string()));
702 out.push(("wav:inst.gain".to_string(), gain.to_string()));
703 out.push(("wav:inst.low_note".to_string(), low_note.to_string()));
704 out.push(("wav:inst.high_note".to_string(), high_note.to_string()));
705 out.push((
706 "wav:inst.low_velocity".to_string(),
707 low_velocity.to_string(),
708 ));
709 out.push((
710 "wav:inst.high_velocity".to_string(),
711 high_velocity.to_string(),
712 ));
713}
714
715fn info_id_to_key(id: &[u8; 4]) -> Option<&'static str> {
716 match id {
717 b"INAM" => Some("title"),
718 b"IART" => Some("artist"),
719 b"IPRD" => Some("album"),
720 b"ICMT" => Some("comment"),
721 b"ICRD" => Some("date"),
722 b"IGNR" => Some("genre"),
723 b"ICOP" => Some("copyright"),
724 b"IENG" => Some("engineer"),
725 b"ITCH" => Some("technician"),
726 b"ISFT" => Some("encoder"),
727 b"ISBJ" => Some("subject"),
728 b"ITRK" => Some("track"),
729 _ => None,
730 }
731}
732
733const BEXT_FIXED_LEN: usize = 602;
742
743fn bext_field(raw: &[u8]) -> String {
747 let end = raw.iter().position(|&b| b == 0).unwrap_or(raw.len());
748 String::from_utf8_lossy(&raw[..end]).trim().to_string()
749}
750
751fn parse_bext_chunk(buf: &[u8], out: &mut Vec<(String, String)>) {
768 if buf.len() < BEXT_FIXED_LEN {
769 return;
770 }
771 let description = bext_field(&buf[0..256]);
772 let originator = bext_field(&buf[256..288]);
773 let originator_ref = bext_field(&buf[288..320]);
774 let origination_date = bext_field(&buf[320..330]);
775 let origination_time = bext_field(&buf[330..338]);
776 let time_ref_low = u32::from_le_bytes([buf[338], buf[339], buf[340], buf[341]]);
777 let time_ref_high = u32::from_le_bytes([buf[342], buf[343], buf[344], buf[345]]);
778 let time_reference = ((time_ref_high as u64) << 32) | (time_ref_low as u64);
779 let version = u16::from_le_bytes([buf[346], buf[347]]);
780
781 let umid = &buf[348..412];
785
786 let loudness_value = i16::from_le_bytes([buf[412], buf[413]]);
788 let loudness_range = i16::from_le_bytes([buf[414], buf[415]]);
789 let max_true_peak = i16::from_le_bytes([buf[416], buf[417]]);
790 let max_momentary = i16::from_le_bytes([buf[418], buf[419]]);
791 let max_short_term = i16::from_le_bytes([buf[420], buf[421]]);
792
793 let coding_history = if buf.len() > BEXT_FIXED_LEN {
795 bext_field(&buf[BEXT_FIXED_LEN..])
796 } else {
797 String::new()
798 };
799
800 let push = |out: &mut Vec<(String, String)>, key: &str, value: String| {
801 if !value.is_empty() {
802 out.push((key.to_string(), value));
803 }
804 };
805
806 push(out, "wav:bext.description", description);
807 push(out, "wav:bext.originator", originator);
808 push(out, "wav:bext.originator_reference", originator_ref);
809 push(out, "wav:bext.origination_date", origination_date);
810 push(out, "wav:bext.origination_time", origination_time);
811 out.push((
812 "wav:bext.time_reference".to_string(),
813 time_reference.to_string(),
814 ));
815 out.push(("wav:bext.version".to_string(), version.to_string()));
816
817 if umid.iter().any(|&b| b != 0) {
820 let mut hex = String::with_capacity(umid.len() * 2);
821 for b in umid {
822 hex.push_str(&format!("{b:02x}"));
823 }
824 out.push(("wav:bext.umid".to_string(), hex));
825 }
826
827 if version >= 2 {
830 out.push((
831 "wav:bext.loudness_value".to_string(),
832 fmt_loudness(loudness_value),
833 ));
834 out.push((
835 "wav:bext.loudness_range".to_string(),
836 fmt_loudness(loudness_range),
837 ));
838 out.push((
839 "wav:bext.max_true_peak_level".to_string(),
840 fmt_loudness(max_true_peak),
841 ));
842 out.push((
843 "wav:bext.max_momentary_loudness".to_string(),
844 fmt_loudness(max_momentary),
845 ));
846 out.push((
847 "wav:bext.max_short_term_loudness".to_string(),
848 fmt_loudness(max_short_term),
849 ));
850 }
851
852 push(out, "wav:bext.coding_history", coding_history);
853}
854
855fn fmt_loudness(v: i16) -> String {
860 let neg = v < 0;
861 let abs = (v as i32).unsigned_abs();
862 let whole = abs / 100;
863 let frac = abs % 100;
864 if neg {
865 format!("-{whole}.{frac:02}")
866 } else {
867 format!("{whole}.{frac:02}")
868 }
869}
870
871#[derive(Clone, Debug)]
872struct WaveFmt {
873 format_tag: u16,
874 channels: u16,
875 sample_rate: u32,
876 #[allow(dead_code)]
877 byte_rate: u32,
878 block_align: u16,
879 bits_per_sample: u16,
880 valid_bits_per_sample: Option<u16>,
884 channel_mask: Option<u32>,
886 subformat: Option<[u8; 16]>,
888}
889
890fn parse_fmt(buf: &[u8]) -> Result<WaveFmt> {
891 if buf.len() < 16 {
892 return Err(Error::invalid("fmt chunk too small"));
893 }
894 let format_tag = u16::from_le_bytes([buf[0], buf[1]]);
895 let channels = u16::from_le_bytes([buf[2], buf[3]]);
896 let sample_rate = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]);
897 let byte_rate = u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]);
898 let block_align = u16::from_le_bytes([buf[12], buf[13]]);
899 let bits_per_sample = u16::from_le_bytes([buf[14], buf[15]]);
900 let mut valid_bits_per_sample = None;
901 let mut channel_mask = None;
902 let mut subformat = None;
903 if format_tag == FMT_EXTENSIBLE {
904 if buf.len() < 40 {
912 return Err(Error::invalid(
913 "WAVE_FORMAT_EXTENSIBLE fmt chunk shorter than the 40 bytes \
914 mandated by mmreg.h",
915 ));
916 }
917 let cb_size = u16::from_le_bytes([buf[16], buf[17]]);
918 if (cb_size as usize) < 22 {
919 return Err(Error::invalid(format!(
920 "WAVE_FORMAT_EXTENSIBLE cbSize must be >= 22, got {cb_size}"
921 )));
922 }
923 valid_bits_per_sample = Some(u16::from_le_bytes([buf[18], buf[19]]));
924 channel_mask = Some(u32::from_le_bytes([buf[20], buf[21], buf[22], buf[23]]));
925 let mut g = [0u8; 16];
926 g.copy_from_slice(&buf[24..40]);
927 subformat = Some(g);
928 }
929 Ok(WaveFmt {
930 format_tag,
931 channels,
932 sample_rate,
933 byte_rate,
934 block_align,
935 bits_per_sample,
936 valid_bits_per_sample,
937 channel_mask,
938 subformat,
939 })
940}
941
942fn resolve_codec(fmt: &WaveFmt) -> Result<CodecId> {
943 match fmt.format_tag {
944 FMT_PCM => Ok(CodecId::new(pcm_int_codec(fmt.bits_per_sample)?)),
945 FMT_IEEE_FLOAT => Ok(CodecId::new(pcm_float_codec(fmt.bits_per_sample)?)),
946 FMT_ALAW => Ok(CodecId::new("pcm_alaw")),
947 FMT_MULAW => Ok(CodecId::new("pcm_mulaw")),
948 FMT_EXTENSIBLE => {
949 let sub = fmt
950 .subformat
951 .ok_or_else(|| Error::invalid("extensible WAV missing subformat"))?;
952 let depth = fmt
959 .valid_bits_per_sample
960 .filter(|&v| v > 0)
961 .unwrap_or(fmt.bits_per_sample);
962 match sub {
963 GUID_PCM => Ok(CodecId::new(pcm_int_codec(depth)?)),
964 GUID_IEEE_FLOAT => Ok(CodecId::new(pcm_float_codec(depth)?)),
965 GUID_ALAW => Ok(CodecId::new("pcm_alaw")),
966 GUID_MULAW => Ok(CodecId::new("pcm_mulaw")),
967 other => Ok(CodecId::new(format!("wav:guid_{}", fmt_guid(&other)))),
972 }
973 }
974 other => Err(Error::unsupported(format!(
975 "unsupported WAV format tag 0x{:04x}",
976 other
977 ))),
978 }
979}
980
981fn pcm_int_codec(bits: u16) -> Result<&'static str> {
982 Ok(match bits {
983 8 => "pcm_u8",
984 16 => "pcm_s16le",
985 24 => "pcm_s24le",
986 32 => "pcm_s32le",
987 _ => {
988 return Err(Error::unsupported(format!(
989 "unsupported WAV integer-PCM bit depth: {bits}"
990 )));
991 }
992 })
993}
994
995fn pcm_float_codec(bits: u16) -> Result<&'static str> {
996 Ok(match bits {
997 32 => "pcm_f32le",
998 64 => "pcm_f64le",
999 _ => {
1000 return Err(Error::unsupported(format!(
1001 "unsupported WAV IEEE-float bit depth: {bits}"
1002 )));
1003 }
1004 })
1005}
1006
1007fn decoded_sample_format(id: &CodecId) -> Option<SampleFormat> {
1017 if let Some(fmt) = super::pcm::sample_format_for(id) {
1019 return Some(fmt);
1020 }
1021 match id.as_str() {
1022 "pcm_alaw" | "pcm_mulaw" => Some(SampleFormat::S16),
1026 _ => None,
1027 }
1028}
1029
1030pub struct WavDemuxer {
1038 input: Box<dyn ReadSeek>,
1039 streams: Vec<StreamInfo>,
1040 data_offset: u64,
1041 data_end: u64,
1042 cursor: u64,
1043 block_align: u64,
1044 chunk_frames: u64,
1045 samples_emitted: i64,
1046 metadata: Vec<(String, String)>,
1047 duration_micros: i64,
1048 format_tag: u16,
1049 valid_bits_per_sample: Option<u16>,
1050 channel_mask: Option<u32>,
1051 subformat: Option<[u8; 16]>,
1052}
1053
1054impl WavDemuxer {
1055 pub fn format_tag(&self) -> u16 {
1059 self.format_tag
1060 }
1061
1062 pub fn valid_bits_per_sample(&self) -> Option<u16> {
1066 self.valid_bits_per_sample
1067 }
1068
1069 pub fn channel_mask(&self) -> Option<u32> {
1076 self.channel_mask
1077 }
1078
1079 pub fn subformat(&self) -> Option<&[u8; 16]> {
1085 self.subformat.as_ref()
1086 }
1087
1088 pub fn subformat_text(&self) -> Option<String> {
1091 self.subformat.as_ref().map(fmt_guid)
1092 }
1093}
1094
1095impl Demuxer for WavDemuxer {
1096 fn format_name(&self) -> &str {
1097 "wav"
1098 }
1099
1100 fn streams(&self) -> &[StreamInfo] {
1101 &self.streams
1102 }
1103
1104 fn next_packet(&mut self) -> Result<Packet> {
1105 if self.cursor >= self.data_end {
1106 return Err(Error::Eof);
1107 }
1108 let remaining = self.data_end - self.cursor;
1109 let want_bytes = (self.chunk_frames * self.block_align).min(remaining);
1110 let want_bytes = (want_bytes / self.block_align) * self.block_align;
1111 if want_bytes == 0 {
1112 return Err(Error::Eof);
1113 }
1114
1115 self.input.seek(SeekFrom::Start(self.cursor))?;
1117 let mut buf = vec![0u8; want_bytes as usize];
1118 self.input.read_exact(&mut buf)?;
1119 self.cursor += want_bytes;
1120
1121 let stream = &self.streams[0];
1122 let frames = want_bytes / self.block_align;
1123 let pts = self.samples_emitted;
1124 self.samples_emitted += frames as i64;
1125
1126 let mut pkt = Packet::new(0, stream.time_base, buf);
1127 pkt.pts = Some(pts);
1128 pkt.dts = Some(pts);
1129 pkt.duration = Some(frames as i64);
1130 pkt.flags.keyframe = true;
1131 Ok(pkt)
1132 }
1133
1134 fn seek_to(&mut self, stream_index: u32, pts: i64) -> Result<i64> {
1135 if stream_index != 0 {
1136 return Err(Error::invalid(format!(
1137 "WAV: stream index {stream_index} out of range"
1138 )));
1139 }
1140 let total_samples = (self.data_end - self.data_offset) / self.block_align;
1144 let target = (pts.max(0) as u64).min(total_samples);
1145 let new_cursor = self.data_offset + target * self.block_align;
1146
1147 self.input.seek(SeekFrom::Start(new_cursor))?;
1148 self.cursor = new_cursor;
1149 self.samples_emitted = target as i64;
1150 Ok(target as i64)
1151 }
1152
1153 fn metadata(&self) -> &[(String, String)] {
1154 &self.metadata
1155 }
1156
1157 fn duration_micros(&self) -> Option<i64> {
1158 if self.duration_micros > 0 {
1159 Some(self.duration_micros)
1160 } else {
1161 None
1162 }
1163 }
1164}
1165
1166fn open_muxer(output: Box<dyn WriteSeek>, streams: &[StreamInfo]) -> Result<Box<dyn Muxer>> {
1169 if streams.len() != 1 {
1170 return Err(Error::unsupported("WAV supports exactly one audio stream"));
1171 }
1172 let s = &streams[0];
1173 if s.params.media_type != MediaType::Audio {
1174 return Err(Error::invalid("WAV stream must be audio"));
1175 }
1176 let channels = s
1177 .params
1178 .channels
1179 .ok_or_else(|| Error::invalid("WAV muxer: missing channels"))?;
1180 let sample_rate = s
1181 .params
1182 .sample_rate
1183 .ok_or_else(|| Error::invalid("WAV muxer: missing sample rate"))?;
1184 let shape = wire_shape_for_params(&s.params)?;
1191 Ok(Box::new(WavMuxer {
1192 output,
1193 channels,
1194 sample_rate,
1195 shape,
1196 extensible: None,
1197 riff_size_offset: 0,
1198 data_size_offset: 0,
1199 data_bytes: 0,
1200 header_written: false,
1201 trailer_written: false,
1202 }))
1203}
1204
1205#[derive(Clone, Debug, Default)]
1217pub struct WavMuxOptions {
1218 extensible: Option<ExtensibleOpts>,
1219}
1220
1221#[derive(Clone, Debug)]
1222struct ExtensibleOpts {
1223 channel_mask: u32,
1224 valid_bits_per_sample: Option<u16>,
1225 subformat: Option<[u8; 16]>,
1226}
1227
1228impl WavMuxOptions {
1229 pub fn with_extensible(mut self, channel_mask: u32) -> Self {
1234 self.extensible = Some(ExtensibleOpts {
1235 channel_mask,
1236 valid_bits_per_sample: None,
1237 subformat: None,
1238 });
1239 self
1240 }
1241
1242 pub fn with_valid_bits_per_sample(mut self, valid_bps: u16) -> Self {
1245 if let Some(opts) = self.extensible.as_mut() {
1246 opts.valid_bits_per_sample = Some(valid_bps);
1247 }
1248 self
1249 }
1250
1251 pub fn with_subformat(mut self, guid: [u8; 16]) -> Self {
1254 if let Some(opts) = self.extensible.as_mut() {
1255 opts.subformat = Some(guid);
1256 }
1257 self
1258 }
1259}
1260
1261pub fn open_muxer_with(
1265 output: Box<dyn WriteSeek>,
1266 streams: &[StreamInfo],
1267 opts: WavMuxOptions,
1268) -> Result<Box<dyn Muxer>> {
1269 if streams.len() != 1 {
1270 return Err(Error::unsupported("WAV supports exactly one audio stream"));
1271 }
1272 let s = &streams[0];
1273 if s.params.media_type != MediaType::Audio {
1274 return Err(Error::invalid("WAV stream must be audio"));
1275 }
1276 let channels = s
1277 .params
1278 .channels
1279 .ok_or_else(|| Error::invalid("WAV muxer: missing channels"))?;
1280 let sample_rate = s
1281 .params
1282 .sample_rate
1283 .ok_or_else(|| Error::invalid("WAV muxer: missing sample rate"))?;
1284 let shape = wire_shape_for_params(&s.params)?;
1285 Ok(Box::new(WavMuxer {
1286 output,
1287 channels,
1288 sample_rate,
1289 shape,
1290 extensible: opts.extensible,
1291 riff_size_offset: 0,
1292 data_size_offset: 0,
1293 data_bytes: 0,
1294 header_written: false,
1295 trailer_written: false,
1296 }))
1297}
1298
1299#[derive(Clone, Copy, Debug)]
1303enum WireShape {
1304 IntPcm { bits: u16 },
1306 FloatPcm { bits: u16 },
1308 Alaw,
1310 Mulaw,
1312}
1313
1314impl WireShape {
1315 fn bits_per_sample(self) -> u16 {
1316 match self {
1317 WireShape::IntPcm { bits } | WireShape::FloatPcm { bits } => bits,
1318 WireShape::Alaw | WireShape::Mulaw => 8,
1319 }
1320 }
1321
1322 fn format_tag(self) -> u16 {
1323 match self {
1324 WireShape::IntPcm { .. } => FMT_PCM,
1325 WireShape::FloatPcm { .. } => FMT_IEEE_FLOAT,
1326 WireShape::Alaw => FMT_ALAW,
1327 WireShape::Mulaw => FMT_MULAW,
1328 }
1329 }
1330
1331 fn well_known_guid(self) -> [u8; 16] {
1332 match self {
1333 WireShape::IntPcm { .. } => GUID_PCM,
1334 WireShape::FloatPcm { .. } => GUID_IEEE_FLOAT,
1335 WireShape::Alaw => GUID_ALAW,
1336 WireShape::Mulaw => GUID_MULAW,
1337 }
1338 }
1339}
1340
1341fn wire_shape_for_params(p: &CodecParameters) -> Result<WireShape> {
1342 match p.codec_id.as_str() {
1343 "pcm_alaw" => return Ok(WireShape::Alaw),
1344 "pcm_mulaw" => return Ok(WireShape::Mulaw),
1345 _ => {}
1346 }
1347 let fmt = p
1348 .sample_format
1349 .or_else(|| super::pcm::sample_format_for(&p.codec_id))
1350 .ok_or_else(|| Error::unsupported(format!("WAV: unknown PCM codec {}", p.codec_id)))?;
1351 Ok(match fmt {
1352 SampleFormat::U8 => WireShape::IntPcm { bits: 8 },
1353 SampleFormat::S16 => WireShape::IntPcm { bits: 16 },
1354 SampleFormat::S24 => WireShape::IntPcm { bits: 24 },
1355 SampleFormat::S32 => WireShape::IntPcm { bits: 32 },
1356 SampleFormat::F32 => WireShape::FloatPcm { bits: 32 },
1357 SampleFormat::F64 => WireShape::FloatPcm { bits: 64 },
1358 other => {
1359 return Err(Error::unsupported(format!(
1360 "WAV muxer cannot write sample format {:?}",
1361 other
1362 )));
1363 }
1364 })
1365}
1366
1367struct WavMuxer {
1368 output: Box<dyn WriteSeek>,
1369 channels: u16,
1370 sample_rate: u32,
1371 shape: WireShape,
1372 extensible: Option<ExtensibleOpts>,
1373 riff_size_offset: u64,
1374 data_size_offset: u64,
1375 data_bytes: u64,
1376 header_written: bool,
1377 trailer_written: bool,
1378}
1379
1380impl Muxer for WavMuxer {
1381 fn format_name(&self) -> &str {
1382 "wav"
1383 }
1384
1385 fn write_header(&mut self) -> Result<()> {
1386 if self.header_written {
1387 return Err(Error::other("WAV header already written"));
1388 }
1389 let bits_per_sample = self.shape.bits_per_sample();
1390 let block_align = (bits_per_sample / 8) * self.channels;
1391 let byte_rate = self.sample_rate * block_align as u32;
1392
1393 let format_tag = if self.extensible.is_some() {
1397 FMT_EXTENSIBLE
1398 } else {
1399 self.shape.format_tag()
1400 };
1401
1402 self.output.write_all(b"RIFF")?;
1403 self.riff_size_offset = self.output.stream_position()?;
1404 self.output.write_all(&0u32.to_le_bytes())?; self.output.write_all(b"WAVE")?;
1406
1407 let fmt_size: u32 = if self.extensible.is_some() { 40 } else { 16 };
1410 self.output.write_all(b"fmt ")?;
1411 self.output.write_all(&fmt_size.to_le_bytes())?;
1412 self.output.write_all(&format_tag.to_le_bytes())?;
1413 self.output.write_all(&self.channels.to_le_bytes())?;
1414 self.output.write_all(&self.sample_rate.to_le_bytes())?;
1415 self.output.write_all(&byte_rate.to_le_bytes())?;
1416 self.output.write_all(&block_align.to_le_bytes())?;
1417 self.output.write_all(&bits_per_sample.to_le_bytes())?;
1418
1419 if let Some(opts) = &self.extensible {
1420 self.output.write_all(&22u16.to_le_bytes())?;
1424 let valid = opts.valid_bits_per_sample.unwrap_or(bits_per_sample);
1425 self.output.write_all(&valid.to_le_bytes())?;
1426 self.output.write_all(&opts.channel_mask.to_le_bytes())?;
1427 let guid = opts
1428 .subformat
1429 .unwrap_or_else(|| self.shape.well_known_guid());
1430 self.output.write_all(&guid)?;
1431 }
1432
1433 self.output.write_all(b"data")?;
1434 self.data_size_offset = self.output.stream_position()?;
1435 self.output.write_all(&0u32.to_le_bytes())?; self.header_written = true;
1438 Ok(())
1439 }
1440
1441 fn write_packet(&mut self, packet: &Packet) -> Result<()> {
1442 if !self.header_written {
1443 return Err(Error::other("WAV muxer: write_header not called"));
1444 }
1445 self.output.write_all(&packet.data)?;
1446 self.data_bytes += packet.data.len() as u64;
1447 Ok(())
1448 }
1449
1450 fn write_trailer(&mut self) -> Result<()> {
1451 if self.trailer_written {
1452 return Ok(());
1453 }
1454 if self.data_bytes % 2 == 1 {
1456 self.output.write_all(&[0u8])?;
1457 }
1458 let end = self.output.stream_position()?;
1459
1460 let data_size_u32: u32 = self
1462 .data_bytes
1463 .try_into()
1464 .map_err(|_| Error::other("WAV data chunk exceeds 4 GiB"))?;
1465 self.output.seek(SeekFrom::Start(self.data_size_offset))?;
1466 self.output.write_all(&data_size_u32.to_le_bytes())?;
1467
1468 let riff_size_u32: u32 = (end - 8)
1470 .try_into()
1471 .map_err(|_| Error::other("WAV RIFF size exceeds 4 GiB"))?;
1472 self.output.seek(SeekFrom::Start(self.riff_size_offset))?;
1473 self.output.write_all(&riff_size_u32.to_le_bytes())?;
1474
1475 self.output.seek(SeekFrom::Start(end))?;
1476 self.output.flush()?;
1477 self.trailer_written = true;
1478 Ok(())
1479 }
1480}
1481
1482#[cfg(test)]
1483mod tests {
1484 use super::*;
1485 use oxideav_core::{CodecParameters, MediaType};
1486
1487 fn make_stream(fmt: SampleFormat, ch: u16, sr: u32) -> StreamInfo {
1488 let mut params = CodecParameters::audio(super::super::pcm::codec_id_for(fmt).unwrap());
1489 params.media_type = MediaType::Audio;
1490 params.channels = Some(ch);
1491 params.sample_rate = Some(sr);
1492 params.sample_format = Some(fmt);
1493 StreamInfo {
1494 index: 0,
1495 time_base: TimeBase::new(1, sr as i64),
1496 duration: None,
1497 start_time: Some(0),
1498 params,
1499 }
1500 }
1501
1502 fn wave_format_tag(p: &CodecParameters) -> Option<u16> {
1503 match p.tag.as_ref()? {
1504 oxideav_core::CodecTag::WaveFormat(t) => Some(*t),
1505 _ => None,
1506 }
1507 }
1508
1509 fn make_g711_stream(codec: &str, ch: u16, sr: u32) -> StreamInfo {
1510 let mut params = CodecParameters::audio(CodecId::new(codec));
1511 params.media_type = MediaType::Audio;
1512 params.channels = Some(ch);
1513 params.sample_rate = Some(sr);
1514 params.sample_format = Some(SampleFormat::S16);
1518 StreamInfo {
1519 index: 0,
1520 time_base: TimeBase::new(1, sr as i64),
1521 duration: None,
1522 start_time: Some(0),
1523 params,
1524 }
1525 }
1526
1527 fn mux_to_bytes(
1533 stream: &StreamInfo,
1534 payload: &[u8],
1535 opts: WavMuxOptions,
1536 tag: &str,
1537 ) -> Vec<u8> {
1538 let tmp = std::env::temp_dir().join(format!("oxideav-basic-wav-r77-{tag}.wav"));
1539 let _ = std::fs::remove_file(&tmp);
1540 {
1541 let f = std::fs::File::create(&tmp).unwrap();
1542 let ws: Box<dyn WriteSeek> = Box::new(f);
1543 let mut mux = open_muxer_with(ws, std::slice::from_ref(stream), opts).unwrap();
1544 mux.write_header().unwrap();
1545 let pkt = Packet::new(0, stream.time_base, payload.to_vec());
1546 mux.write_packet(&pkt).unwrap();
1547 mux.write_trailer().unwrap();
1548 }
1549 let bytes = std::fs::read(&tmp).unwrap();
1550 let _ = std::fs::remove_file(&tmp);
1551 bytes
1552 }
1553
1554 fn open_demux_from_bytes(bytes: Vec<u8>) -> Box<dyn Demuxer> {
1555 use std::io::Cursor;
1556 let rs: Box<dyn ReadSeek> = Box::new(Cursor::new(bytes));
1557 open_demuxer(rs, &oxideav_core::NullCodecResolver).unwrap()
1558 }
1559
1560 #[test]
1561 fn round_trip_s16_mono() {
1562 let samples: Vec<i16> = (0..1000).map(|i| ((i * 32) - 16000) as i16).collect();
1564 let mut payload = Vec::with_capacity(samples.len() * 2);
1565 for s in &samples {
1566 payload.extend_from_slice(&s.to_le_bytes());
1567 }
1568
1569 let stream = make_stream(SampleFormat::S16, 1, 48_000);
1571 let tmp = std::env::temp_dir().join("oxideav-basic-wav-test.wav");
1572 {
1573 let f = std::fs::File::create(&tmp).unwrap();
1574 let ws: Box<dyn WriteSeek> = Box::new(f);
1575 let mut mux = open_muxer(ws, std::slice::from_ref(&stream)).unwrap();
1576 mux.write_header().unwrap();
1577 let pkt = Packet::new(0, stream.time_base, payload.clone());
1578 mux.write_packet(&pkt).unwrap();
1579 mux.write_trailer().unwrap();
1580 }
1581 let rs: Box<dyn ReadSeek> = Box::new(std::fs::File::open(&tmp).unwrap());
1582 let mut dmx = open_demuxer(rs, &oxideav_core::NullCodecResolver).unwrap();
1583 assert_eq!(dmx.format_name(), "wav");
1584 assert_eq!(dmx.streams().len(), 1);
1585 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
1586 let mut out_bytes = Vec::new();
1587 loop {
1588 match dmx.next_packet() {
1589 Ok(p) => out_bytes.extend_from_slice(&p.data),
1590 Err(Error::Eof) => break,
1591 Err(e) => panic!("demux error: {e}"),
1592 }
1593 }
1594 assert_eq!(out_bytes, payload);
1595 }
1596
1597 #[test]
1600 fn round_trip_alaw_mono() {
1601 let payload: Vec<u8> = (0..=255u8).collect();
1604 let stream = make_g711_stream("pcm_alaw", 1, 8_000);
1605 let bytes = mux_to_bytes(&stream, &payload, WavMuxOptions::default(), "alaw-mono");
1606 let mut dmx = open_demux_from_bytes(bytes);
1607 assert_eq!(dmx.streams().len(), 1);
1608 let s = &dmx.streams()[0];
1609 assert_eq!(s.params.codec_id, CodecId::new("pcm_alaw"));
1610 assert_eq!(wave_format_tag(&s.params), Some(FMT_ALAW));
1611 assert_eq!(s.params.sample_format, Some(SampleFormat::S16));
1612 assert_eq!(s.params.bit_rate, Some(8 * 8_000));
1615 let mut out = Vec::new();
1616 loop {
1617 match dmx.next_packet() {
1618 Ok(p) => out.extend_from_slice(&p.data),
1619 Err(Error::Eof) => break,
1620 Err(e) => panic!("demux error: {e}"),
1621 }
1622 }
1623 assert_eq!(out, payload);
1624 }
1625
1626 #[test]
1628 fn round_trip_mulaw_stereo() {
1629 let payload: Vec<u8> = (0..512u32).map(|i| (i & 0xFF) as u8).collect();
1630 let stream = make_g711_stream("pcm_mulaw", 2, 8_000);
1631 let bytes = mux_to_bytes(&stream, &payload, WavMuxOptions::default(), "mulaw-stereo");
1632 let mut dmx = open_demux_from_bytes(bytes);
1633 let s = &dmx.streams()[0];
1634 assert_eq!(s.params.codec_id, CodecId::new("pcm_mulaw"));
1635 assert_eq!(wave_format_tag(&s.params), Some(FMT_MULAW));
1636 assert_eq!(s.params.bit_rate, Some(16 * 8_000));
1639 let mut out = Vec::new();
1640 loop {
1641 match dmx.next_packet() {
1642 Ok(p) => out.extend_from_slice(&p.data),
1643 Err(Error::Eof) => break,
1644 Err(e) => panic!("demux error: {e}"),
1645 }
1646 }
1647 assert_eq!(out, payload);
1648 }
1649
1650 #[test]
1655 fn round_trip_extensible_5_1_pcm() {
1656 const MASK_5_1: u32 = 0x0003F;
1662 let frame: [i16; 6] = [-100, 200, -300, 400, -500, 600];
1663 let mut payload = Vec::new();
1664 for _ in 0..32 {
1665 for s in &frame {
1666 payload.extend_from_slice(&s.to_le_bytes());
1667 }
1668 }
1669
1670 let mut stream = make_stream(SampleFormat::S16, 6, 48_000);
1671 stream.params.codec_id = CodecId::new("pcm_s16le");
1675 let opts = WavMuxOptions::default().with_extensible(MASK_5_1);
1676 let buf = mux_to_bytes(&stream, &payload, opts, "ext-5-1-pcm");
1677
1678 assert_eq!(&buf[12..16], b"fmt ");
1682 let fmt_size = u32::from_le_bytes([buf[16], buf[17], buf[18], buf[19]]);
1683 assert_eq!(fmt_size, 40, "EXTENSIBLE fmt chunk must be 40 bytes");
1684 assert_eq!(u16::from_le_bytes([buf[20], buf[21]]), FMT_EXTENSIBLE);
1686 assert_eq!(u16::from_le_bytes([buf[36], buf[37]]), 22);
1688
1689 let dmx = open_demux_from_bytes(buf);
1691 let s = &dmx.streams()[0];
1695 assert_eq!(s.params.codec_id, CodecId::new("pcm_s16le"));
1696 assert_eq!(wave_format_tag(&s.params), Some(FMT_EXTENSIBLE));
1697
1698 let md: std::collections::HashMap<String, String> =
1700 dmx.metadata().iter().cloned().collect();
1701 assert_eq!(
1702 md.get("wav:fmt.channel_mask"),
1703 Some(&format!("0x{MASK_5_1:08X}"))
1704 );
1705 assert_eq!(
1706 md.get("wav:fmt.valid_bits_per_sample"),
1707 Some(&"16".to_string())
1708 );
1709 assert_eq!(
1710 md.get("wav:fmt.subformat"),
1711 Some(&"00000001-0000-0010-8000-00AA00389B71".to_string())
1712 );
1713 }
1714
1715 #[test]
1718 fn extensible_accessors_match_metadata() {
1719 const MASK_STEREO: u32 = 0x00003;
1720 let payload = vec![0u8; 4 * 100]; let mut stream = make_stream(SampleFormat::S16, 2, 44_100);
1723 stream.params.codec_id = CodecId::new("pcm_s16le");
1724
1725 let opts = WavMuxOptions::default().with_extensible(MASK_STEREO);
1726 let bytes = mux_to_bytes(&stream, &payload, opts, "ext-stereo-md");
1727 let dmx = open_demux_from_bytes(bytes);
1728 let md: std::collections::HashMap<String, String> =
1729 dmx.metadata().iter().cloned().collect();
1730 assert_eq!(
1731 md.get("wav:fmt.channel_mask"),
1732 Some(&format!("0x{MASK_STEREO:08X}"))
1733 );
1734 assert_eq!(
1735 md.get("wav:fmt.valid_bits_per_sample"),
1736 Some(&"16".to_string())
1737 );
1738 }
1739
1740 #[test]
1744 fn extensible_alaw_through_guid() {
1745 let payload: Vec<u8> = (0..=255u8).collect();
1746 let stream = make_g711_stream("pcm_alaw", 1, 8_000);
1747
1748 let opts = WavMuxOptions::default().with_extensible(0x00004); let bytes = mux_to_bytes(&stream, &payload, opts, "ext-alaw");
1750 let mut dmx = open_demux_from_bytes(bytes);
1751 let s = &dmx.streams()[0];
1752 assert_eq!(s.params.codec_id, CodecId::new("pcm_alaw"));
1757 assert_eq!(wave_format_tag(&s.params), Some(FMT_EXTENSIBLE));
1758 let mut out = Vec::new();
1759 loop {
1760 match dmx.next_packet() {
1761 Ok(p) => out.extend_from_slice(&p.data),
1762 Err(Error::Eof) => break,
1763 Err(e) => panic!("demux error: {e}"),
1764 }
1765 }
1766 assert_eq!(out, payload);
1767 }
1768
1769 #[test]
1774 fn extensible_unknown_guid_synthesised_id() {
1775 let bogus_guid: [u8; 16] = [
1779 0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC,
1780 0xDE, 0xF0,
1781 ];
1782 let mut buf = Vec::new();
1783 buf.extend_from_slice(b"RIFF");
1784 buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(b"WAVE");
1786 buf.extend_from_slice(b"fmt ");
1787 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");
1799 buf.extend_from_slice(&0u32.to_le_bytes()); use std::io::Cursor;
1802 let rs: Box<dyn ReadSeek> = Box::new(Cursor::new(buf));
1803 let dmx_res = open_demuxer(rs, &oxideav_core::NullCodecResolver);
1804 let dmx = dmx_res.expect("unknown-GUID extensible stream still parses");
1805 let id = dmx.streams()[0].params.codec_id.as_str().to_string();
1806 assert!(
1807 id.starts_with("wav:guid_"),
1808 "unknown GUID must synthesise wav:guid_<text>, got {id:?}"
1809 );
1810 assert!(
1813 id.contains("EFBEADDE-FECA-BEBA"),
1814 "synthesised id must carry the canonical GUID text, got {id:?}"
1815 );
1816 }
1817
1818 #[test]
1824 fn extensible_short_cbsize_rejected() {
1825 let mut buf = Vec::new();
1826 buf.extend_from_slice(b"RIFF");
1827 buf.extend_from_slice(&0u32.to_le_bytes());
1828 buf.extend_from_slice(b"WAVE");
1829 buf.extend_from_slice(b"fmt ");
1830 buf.extend_from_slice(&40u32.to_le_bytes());
1832 buf.extend_from_slice(&FMT_EXTENSIBLE.to_le_bytes());
1833 buf.extend_from_slice(&1u16.to_le_bytes());
1834 buf.extend_from_slice(&44_100u32.to_le_bytes());
1835 buf.extend_from_slice(&88_200u32.to_le_bytes());
1836 buf.extend_from_slice(&2u16.to_le_bytes());
1837 buf.extend_from_slice(&16u16.to_le_bytes());
1838 buf.extend_from_slice(&10u16.to_le_bytes()); buf.extend_from_slice(&[0u8; 20]); buf.extend_from_slice(b"data");
1841 buf.extend_from_slice(&0u32.to_le_bytes());
1842
1843 use std::io::Cursor;
1844 let rs: Box<dyn ReadSeek> = Box::new(Cursor::new(buf));
1845 let err = open_demuxer(rs, &oxideav_core::NullCodecResolver).err();
1846 assert!(
1847 matches!(err, Some(Error::InvalidData(_))),
1848 "short cbSize must yield Error::InvalidData, got {err:?}"
1849 );
1850 }
1851
1852 fn wav_with_bext(bext_body: &[u8]) -> Vec<u8> {
1857 let mut buf = Vec::new();
1858 buf.extend_from_slice(b"RIFF");
1859 buf.extend_from_slice(&0u32.to_le_bytes());
1860 buf.extend_from_slice(b"WAVE");
1861 buf.extend_from_slice(b"fmt ");
1863 buf.extend_from_slice(&16u32.to_le_bytes());
1864 buf.extend_from_slice(&FMT_PCM.to_le_bytes());
1865 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");
1872 buf.extend_from_slice(&(bext_body.len() as u32).to_le_bytes());
1873 buf.extend_from_slice(bext_body);
1874 if bext_body.len() % 2 == 1 {
1875 buf.push(0); }
1877 buf.extend_from_slice(b"data");
1879 buf.extend_from_slice(&0u32.to_le_bytes());
1880 buf
1881 }
1882
1883 #[allow(clippy::too_many_arguments)]
1886 fn make_bext_v2(
1887 description: &str,
1888 originator: &str,
1889 originator_ref: &str,
1890 date: &str,
1891 time: &str,
1892 time_reference: u64,
1893 umid: &[u8; 64],
1894 loudness: [i16; 5],
1895 coding_history: &str,
1896 ) -> Vec<u8> {
1897 fn put_ascii(buf: &mut Vec<u8>, s: &str, width: usize) {
1898 let bytes = s.as_bytes();
1899 let n = bytes.len().min(width);
1900 buf.extend_from_slice(&bytes[..n]);
1901 buf.resize(buf.len() + (width - n), 0);
1902 }
1903 let mut b = Vec::new();
1904 put_ascii(&mut b, description, 256);
1905 put_ascii(&mut b, originator, 32);
1906 put_ascii(&mut b, originator_ref, 32);
1907 put_ascii(&mut b, date, 10);
1908 put_ascii(&mut b, time, 8);
1909 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);
1913 for v in &loudness {
1914 b.extend_from_slice(&v.to_le_bytes());
1915 }
1916 b.resize(BEXT_FIXED_LEN, 0); assert_eq!(b.len(), BEXT_FIXED_LEN);
1918 b.extend_from_slice(coding_history.as_bytes());
1919 b
1920 }
1921
1922 #[test]
1928 fn bext_loudness_formatting() {
1929 assert_eq!(fmt_loudness(-2264), "-22.64");
1930 assert_eq!(fmt_loudness(-2265), "-22.65");
1931 assert_eq!(fmt_loudness(0), "0.00");
1932 assert_eq!(fmt_loudness(2300), "23.00");
1933 assert_eq!(fmt_loudness(-50), "-0.50"); assert_eq!(fmt_loudness(7), "0.07");
1935 assert_eq!(fmt_loudness(-1), "-0.01");
1936 }
1937
1938 #[test]
1943 fn bext_v2_full_metadata() {
1944 let mut umid = [0u8; 64];
1945 umid[0] = 0x06;
1946 umid[1] = 0x0a;
1947 umid[63] = 0xff;
1948 let body = make_bext_v2(
1949 "Scene 1 take 3",
1950 "OxideAV Recorder",
1951 "USABC2400001",
1952 "2026-05-23",
1953 "14:30:00",
1954 0x0000_0001_2345_6789,
1956 &umid,
1957 [-2305, 700, -120, -1850, -2010],
1958 "A=PCM,F=48000,W=24,M=stereo,T=OxideAV\r\n",
1959 );
1960 let bytes = wav_with_bext(&body);
1961 let dmx = open_demux_from_bytes(bytes);
1962 let md: std::collections::HashMap<String, String> =
1963 dmx.metadata().iter().cloned().collect();
1964
1965 assert_eq!(
1966 md.get("wav:bext.description"),
1967 Some(&"Scene 1 take 3".to_string())
1968 );
1969 assert_eq!(
1970 md.get("wav:bext.originator"),
1971 Some(&"OxideAV Recorder".to_string())
1972 );
1973 assert_eq!(
1974 md.get("wav:bext.originator_reference"),
1975 Some(&"USABC2400001".to_string())
1976 );
1977 assert_eq!(
1978 md.get("wav:bext.origination_date"),
1979 Some(&"2026-05-23".to_string())
1980 );
1981 assert_eq!(
1982 md.get("wav:bext.origination_time"),
1983 Some(&"14:30:00".to_string())
1984 );
1985 assert_eq!(
1986 md.get("wav:bext.time_reference"),
1987 Some(&0x0000_0001_2345_6789u64.to_string())
1988 );
1989 assert_eq!(md.get("wav:bext.version"), Some(&"2".to_string()));
1990 assert_eq!(
1991 md.get("wav:bext.loudness_value"),
1992 Some(&"-23.05".to_string())
1993 );
1994 assert_eq!(md.get("wav:bext.loudness_range"), Some(&"7.00".to_string()));
1995 assert_eq!(
1996 md.get("wav:bext.max_true_peak_level"),
1997 Some(&"-1.20".to_string())
1998 );
1999 assert_eq!(
2000 md.get("wav:bext.max_momentary_loudness"),
2001 Some(&"-18.50".to_string())
2002 );
2003 assert_eq!(
2004 md.get("wav:bext.max_short_term_loudness"),
2005 Some(&"-20.10".to_string())
2006 );
2007 let umid_hex = md.get("wav:bext.umid").expect("umid present");
2009 assert!(umid_hex.starts_with("060a"), "umid hex {umid_hex:?}");
2010 assert!(umid_hex.ends_with("ff"), "umid hex {umid_hex:?}");
2011 assert_eq!(umid_hex.len(), 128); assert_eq!(
2013 md.get("wav:bext.coding_history"),
2014 Some(&"A=PCM,F=48000,W=24,M=stereo,T=OxideAV".to_string())
2015 );
2016 }
2017
2018 #[test]
2022 fn bext_v0_omits_umid_and_loudness() {
2023 let mut body = vec![0u8; BEXT_FIXED_LEN];
2025 let desc = b"Field recording";
2027 body[..desc.len()].copy_from_slice(desc);
2028 body[338..342].copy_from_slice(&12_345u32.to_le_bytes());
2030 let bytes = wav_with_bext(&body);
2032 let dmx = open_demux_from_bytes(bytes);
2033 let md: std::collections::HashMap<String, String> =
2034 dmx.metadata().iter().cloned().collect();
2035
2036 assert_eq!(
2037 md.get("wav:bext.description"),
2038 Some(&"Field recording".to_string())
2039 );
2040 assert_eq!(md.get("wav:bext.version"), Some(&"0".to_string()));
2041 assert_eq!(
2042 md.get("wav:bext.time_reference"),
2043 Some(&"12345".to_string())
2044 );
2045 assert!(!md.contains_key("wav:bext.umid"));
2047 assert!(!md.contains_key("wav:bext.loudness_value"));
2048 assert!(!md.contains_key("wav:bext.max_true_peak_level"));
2049 }
2050
2051 #[test]
2055 fn bext_truncated_is_skipped() {
2056 let body = vec![0u8; 100]; let bytes = wav_with_bext(&body);
2058 let dmx = open_demux_from_bytes(bytes);
2059 let md: std::collections::HashMap<String, String> =
2060 dmx.metadata().iter().cloned().collect();
2061 assert!(md.keys().all(|k| !k.starts_with("wav:bext.")));
2062 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
2064 }
2065
2066 fn wav_with_cue_and_adtl(cue_body: &[u8], adtl_body: Option<&[u8]>) -> Vec<u8> {
2070 let mut buf = Vec::new();
2071 buf.extend_from_slice(b"RIFF");
2072 buf.extend_from_slice(&0u32.to_le_bytes());
2073 buf.extend_from_slice(b"WAVE");
2074 buf.extend_from_slice(b"fmt ");
2076 buf.extend_from_slice(&16u32.to_le_bytes());
2077 buf.extend_from_slice(&FMT_PCM.to_le_bytes());
2078 buf.extend_from_slice(&1u16.to_le_bytes());
2079 buf.extend_from_slice(&8_000u32.to_le_bytes());
2080 buf.extend_from_slice(&16_000u32.to_le_bytes());
2081 buf.extend_from_slice(&2u16.to_le_bytes());
2082 buf.extend_from_slice(&16u16.to_le_bytes());
2083 buf.extend_from_slice(b"cue ");
2085 buf.extend_from_slice(&(cue_body.len() as u32).to_le_bytes());
2086 buf.extend_from_slice(cue_body);
2087 if cue_body.len() % 2 == 1 {
2088 buf.push(0);
2089 }
2090 if let Some(adtl) = adtl_body {
2092 buf.extend_from_slice(b"LIST");
2094 buf.extend_from_slice(&((adtl.len() + 4) as u32).to_le_bytes());
2095 buf.extend_from_slice(b"adtl");
2096 buf.extend_from_slice(adtl);
2097 if (adtl.len() + 4) % 2 == 1 {
2098 buf.push(0);
2099 }
2100 }
2101 buf.extend_from_slice(b"data");
2103 buf.extend_from_slice(&0u32.to_le_bytes());
2104 buf
2105 }
2106
2107 fn cue_point(
2110 dw_name: u32,
2111 dw_position: u32,
2112 fcc_chunk: &[u8; 4],
2113 dw_chunk_start: u32,
2114 dw_block_start: u32,
2115 dw_sample_offset: u32,
2116 ) -> Vec<u8> {
2117 let mut b = Vec::with_capacity(24);
2118 b.extend_from_slice(&dw_name.to_le_bytes());
2119 b.extend_from_slice(&dw_position.to_le_bytes());
2120 b.extend_from_slice(fcc_chunk);
2121 b.extend_from_slice(&dw_chunk_start.to_le_bytes());
2122 b.extend_from_slice(&dw_block_start.to_le_bytes());
2123 b.extend_from_slice(&dw_sample_offset.to_le_bytes());
2124 b
2125 }
2126
2127 fn adtl_text_subchunk(id: &[u8; 4], dw_name: u32, text: &str) -> Vec<u8> {
2130 let mut b = Vec::new();
2131 b.extend_from_slice(id);
2132 let body_len = 4 + text.len() + 1;
2134 b.extend_from_slice(&(body_len as u32).to_le_bytes());
2135 b.extend_from_slice(&dw_name.to_le_bytes());
2136 b.extend_from_slice(text.as_bytes());
2137 b.push(0);
2138 if body_len % 2 == 1 {
2139 b.push(0);
2140 }
2141 b
2142 }
2143
2144 #[test]
2148 fn cue_and_adtl_full_metadata() {
2149 let mut cue_body = Vec::new();
2151 cue_body.extend_from_slice(&2u32.to_le_bytes()); cue_body.extend(cue_point(1, 0, b"data", 0, 0, 0));
2153 cue_body.extend(cue_point(2, 12_345, b"data", 0, 0, 12_345));
2154
2155 let mut adtl = Vec::new();
2157 adtl.extend(adtl_text_subchunk(b"labl", 1, "Intro"));
2158 adtl.extend(adtl_text_subchunk(b"note", 1, "Fade-in"));
2159 adtl.extend(adtl_text_subchunk(b"labl", 2, "Verse"));
2160 adtl.extend(adtl_text_subchunk(b"note", 2, "Vocal entry"));
2161
2162 let bytes = wav_with_cue_and_adtl(&cue_body, Some(&adtl));
2163 let dmx = open_demux_from_bytes(bytes);
2164 let md: std::collections::HashMap<String, String> =
2165 dmx.metadata().iter().cloned().collect();
2166
2167 assert_eq!(md.get("wav:cue.count"), Some(&"2".to_string()));
2168 assert_eq!(md.get("wav:cue.1.position"), Some(&"0".to_string()));
2169 assert_eq!(md.get("wav:cue.1.fcc_chunk"), Some(&"data".to_string()));
2170 assert_eq!(md.get("wav:cue.1.chunk_start"), Some(&"0".to_string()));
2171 assert_eq!(md.get("wav:cue.1.block_start"), Some(&"0".to_string()));
2172 assert_eq!(md.get("wav:cue.1.sample_offset"), Some(&"0".to_string()));
2173 assert_eq!(md.get("wav:cue.2.position"), Some(&"12345".to_string()));
2174 assert_eq!(
2175 md.get("wav:cue.2.sample_offset"),
2176 Some(&"12345".to_string())
2177 );
2178
2179 assert_eq!(md.get("wav:adtl.labl.1"), Some(&"Intro".to_string()));
2180 assert_eq!(md.get("wav:adtl.note.1"), Some(&"Fade-in".to_string()));
2181 assert_eq!(md.get("wav:adtl.labl.2"), Some(&"Verse".to_string()));
2182 assert_eq!(md.get("wav:adtl.note.2"), Some(&"Vocal entry".to_string()));
2183 }
2184
2185 #[test]
2188 fn adtl_ltxt_segment_metadata() {
2189 let mut cue_body = Vec::new();
2191 cue_body.extend_from_slice(&1u32.to_le_bytes());
2192 cue_body.extend(cue_point(7, 1000, b"data", 0, 0, 1000));
2193
2194 let mut ltxt_body = Vec::new();
2196 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(&0u16.to_le_bytes()); ltxt_body.extend_from_slice(&0u16.to_le_bytes()); ltxt_body.extend_from_slice(&0u16.to_le_bytes()); ltxt_body.extend_from_slice(&0u16.to_le_bytes()); ltxt_body.extend_from_slice(b"Hello world");
2204 ltxt_body.push(0);
2205
2206 let mut adtl = Vec::new();
2207 adtl.extend_from_slice(b"ltxt");
2208 adtl.extend_from_slice(&(ltxt_body.len() as u32).to_le_bytes());
2209 adtl.extend_from_slice(<xt_body);
2210 if ltxt_body.len() % 2 == 1 {
2211 adtl.push(0);
2212 }
2213
2214 let bytes = wav_with_cue_and_adtl(&cue_body, Some(&adtl));
2215 let dmx = open_demux_from_bytes(bytes);
2216 let md: std::collections::HashMap<String, String> =
2217 dmx.metadata().iter().cloned().collect();
2218
2219 assert_eq!(md.get("wav:adtl.ltxt.7.length"), Some(&"4410".to_string()));
2220 assert_eq!(md.get("wav:adtl.ltxt.7.purpose"), Some(&"scrp".to_string()));
2221 assert_eq!(
2222 md.get("wav:adtl.ltxt.7.text"),
2223 Some(&"Hello world".to_string())
2224 );
2225 }
2226
2227 #[test]
2231 fn cue_truncated_count_is_clamped() {
2232 let mut cue_body = Vec::new();
2234 cue_body.extend_from_slice(&5u32.to_le_bytes());
2235 cue_body.extend(cue_point(42, 100, b"data", 0, 0, 100));
2236 let bytes = wav_with_cue_and_adtl(&cue_body, None);
2237 let dmx = open_demux_from_bytes(bytes);
2238 let md: std::collections::HashMap<String, String> =
2239 dmx.metadata().iter().cloned().collect();
2240 assert_eq!(md.get("wav:cue.count"), Some(&"1".to_string()));
2241 assert_eq!(md.get("wav:cue.42.position"), Some(&"100".to_string()));
2242 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
2244 }
2245
2246 #[test]
2251 fn adtl_without_cue_still_surfaces() {
2252 let mut adtl = Vec::new();
2253 adtl.extend(adtl_text_subchunk(b"labl", 99, "Orphan label"));
2254 let cue_body = 0u32.to_le_bytes().to_vec();
2256 let bytes = wav_with_cue_and_adtl(&cue_body, Some(&adtl));
2257 let dmx = open_demux_from_bytes(bytes);
2258 let md: std::collections::HashMap<String, String> =
2259 dmx.metadata().iter().cloned().collect();
2260 assert_eq!(md.get("wav:cue.count"), Some(&"0".to_string()));
2261 assert_eq!(
2262 md.get("wav:adtl.labl.99"),
2263 Some(&"Orphan label".to_string())
2264 );
2265 }
2266
2267 fn wav_with_smpl_and_inst(smpl_body: Option<&[u8]>, inst_body: Option<&[u8]>) -> Vec<u8> {
2272 let mut buf = Vec::new();
2273 buf.extend_from_slice(b"RIFF");
2274 buf.extend_from_slice(&0u32.to_le_bytes());
2275 buf.extend_from_slice(b"WAVE");
2276 buf.extend_from_slice(b"fmt ");
2278 buf.extend_from_slice(&16u32.to_le_bytes());
2279 buf.extend_from_slice(&FMT_PCM.to_le_bytes());
2280 buf.extend_from_slice(&1u16.to_le_bytes());
2281 buf.extend_from_slice(&8_000u32.to_le_bytes());
2282 buf.extend_from_slice(&16_000u32.to_le_bytes());
2283 buf.extend_from_slice(&2u16.to_le_bytes());
2284 buf.extend_from_slice(&16u16.to_le_bytes());
2285 if let Some(smpl) = smpl_body {
2286 buf.extend_from_slice(b"smpl");
2287 buf.extend_from_slice(&(smpl.len() as u32).to_le_bytes());
2288 buf.extend_from_slice(smpl);
2289 if smpl.len() % 2 == 1 {
2290 buf.push(0);
2291 }
2292 }
2293 if let Some(inst) = inst_body {
2294 buf.extend_from_slice(b"inst");
2295 buf.extend_from_slice(&(inst.len() as u32).to_le_bytes());
2296 buf.extend_from_slice(inst);
2297 if inst.len() % 2 == 1 {
2298 buf.push(0);
2299 }
2300 }
2301 buf.extend_from_slice(b"data");
2303 buf.extend_from_slice(&0u32.to_le_bytes());
2304 buf
2305 }
2306
2307 #[allow(clippy::too_many_arguments)]
2310 fn smpl_body(
2311 manufacturer: u32,
2312 product: u32,
2313 sample_period: u32,
2314 midi_unity_note: u32,
2315 midi_pitch_fraction: u32,
2316 smpte_format: u32,
2317 smpte_offset: u32,
2318 c_sample_loops_claimed: u32,
2319 cb_sampler_data: u32,
2320 loops: &[(u32, u32, u32, u32, u32, u32)],
2321 ) -> Vec<u8> {
2322 let mut b = Vec::new();
2323 for v in [
2324 manufacturer,
2325 product,
2326 sample_period,
2327 midi_unity_note,
2328 midi_pitch_fraction,
2329 smpte_format,
2330 smpte_offset,
2331 c_sample_loops_claimed,
2332 cb_sampler_data,
2333 ] {
2334 b.extend_from_slice(&v.to_le_bytes());
2335 }
2336 for &(id, ty, start, end, frac, count) in loops {
2337 for v in [id, ty, start, end, frac, count] {
2338 b.extend_from_slice(&v.to_le_bytes());
2339 }
2340 }
2341 b
2342 }
2343
2344 #[test]
2348 fn smpl_full_metadata() {
2349 let body = smpl_body(
2351 0x1234, 0xDEAD_BEEF, 22_675, 60, 0x8000_0000, 30, 0x01_02_03_04, 1, 0, &[(7, 0, 0, 1000, 0, 0)], );
2362 let bytes = wav_with_smpl_and_inst(Some(&body), None);
2363 let dmx = open_demux_from_bytes(bytes);
2364 let md: std::collections::HashMap<String, String> =
2365 dmx.metadata().iter().cloned().collect();
2366 assert_eq!(md.get("wav:smpl.manufacturer"), Some(&"4660".to_string()));
2367 assert_eq!(md.get("wav:smpl.product"), Some(&"3735928559".to_string()));
2368 assert_eq!(md.get("wav:smpl.sample_period"), Some(&"22675".to_string()));
2369 assert_eq!(md.get("wav:smpl.midi_unity_note"), Some(&"60".to_string()));
2370 assert_eq!(
2371 md.get("wav:smpl.midi_pitch_fraction"),
2372 Some(&"2147483648".to_string())
2373 );
2374 assert_eq!(md.get("wav:smpl.smpte_format"), Some(&"30".to_string()));
2375 assert_eq!(
2376 md.get("wav:smpl.smpte_offset"),
2377 Some(&"01:02:03:04".to_string())
2378 );
2379 assert_eq!(md.get("wav:smpl.sampler_data_len"), Some(&"0".to_string()));
2380 assert_eq!(md.get("wav:smpl.num_sample_loops"), Some(&"1".to_string()));
2381 assert_eq!(
2382 md.get("wav:smpl.loop.0.cue_point_id"),
2383 Some(&"7".to_string())
2384 );
2385 assert_eq!(md.get("wav:smpl.loop.0.type"), Some(&"0".to_string()));
2386 assert_eq!(md.get("wav:smpl.loop.0.start"), Some(&"0".to_string()));
2387 assert_eq!(md.get("wav:smpl.loop.0.end"), Some(&"1000".to_string()));
2388 assert_eq!(md.get("wav:smpl.loop.0.fraction"), Some(&"0".to_string()));
2389 assert_eq!(md.get("wav:smpl.loop.0.play_count"), Some(&"0".to_string()));
2390 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
2392 }
2393
2394 #[test]
2399 fn smpl_loop_count_clamped_to_body() {
2400 let body = smpl_body(
2403 0,
2404 0,
2405 0,
2406 60,
2407 0,
2408 0,
2409 0,
2410 5,
2411 0,
2412 &[(1, 0, 0, 100, 0, 0)],
2413 );
2414 let bytes = wav_with_smpl_and_inst(Some(&body), None);
2415 let dmx = open_demux_from_bytes(bytes);
2416 let md: std::collections::HashMap<String, String> =
2417 dmx.metadata().iter().cloned().collect();
2418 assert_eq!(md.get("wav:smpl.num_sample_loops"), Some(&"1".to_string()));
2419 assert!(md.contains_key("wav:smpl.loop.0.cue_point_id"));
2421 assert!(!md.contains_key("wav:smpl.loop.1.cue_point_id"));
2422 assert!(!md.contains_key("wav:smpl.loop.4.cue_point_id"));
2423 }
2424
2425 #[test]
2429 fn smpl_truncated_is_skipped() {
2430 let body = vec![0u8; 20]; let bytes = wav_with_smpl_and_inst(Some(&body), None);
2432 let dmx = open_demux_from_bytes(bytes);
2433 let md: std::collections::HashMap<String, String> =
2434 dmx.metadata().iter().cloned().collect();
2435 assert!(md.keys().all(|k| !k.starts_with("wav:smpl.")));
2436 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
2437 }
2438
2439 #[test]
2443 fn inst_full_metadata() {
2444 let body: Vec<u8> = vec![60, 0xFD, 0xFA, 36, 96, 1, 127];
2446 let bytes = wav_with_smpl_and_inst(None, Some(&body));
2447 let dmx = open_demux_from_bytes(bytes);
2448 let md: std::collections::HashMap<String, String> =
2449 dmx.metadata().iter().cloned().collect();
2450 assert_eq!(md.get("wav:inst.unshifted_note"), Some(&"60".to_string()));
2451 assert_eq!(md.get("wav:inst.fine_tune"), Some(&"-3".to_string()));
2452 assert_eq!(md.get("wav:inst.gain"), Some(&"-6".to_string()));
2453 assert_eq!(md.get("wav:inst.low_note"), Some(&"36".to_string()));
2454 assert_eq!(md.get("wav:inst.high_note"), Some(&"96".to_string()));
2455 assert_eq!(md.get("wav:inst.low_velocity"), Some(&"1".to_string()));
2456 assert_eq!(md.get("wav:inst.high_velocity"), Some(&"127".to_string()));
2457 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
2458 }
2459
2460 #[test]
2463 fn inst_truncated_is_skipped() {
2464 let body = vec![0u8; 5]; let bytes = wav_with_smpl_and_inst(None, Some(&body));
2466 let dmx = open_demux_from_bytes(bytes);
2467 let md: std::collections::HashMap<String, String> =
2468 dmx.metadata().iter().cloned().collect();
2469 assert!(md.keys().all(|k| !k.starts_with("wav:inst.")));
2470 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
2471 }
2472
2473 #[test]
2478 fn smpl_and_inst_coexist_with_padding() {
2479 let smpl = smpl_body(0, 0, 0, 64, 0, 0, 0, 0, 0, &[]);
2480 let inst: Vec<u8> = vec![64, 0, 0, 0, 127, 1, 127]; let bytes = wav_with_smpl_and_inst(Some(&smpl), Some(&inst));
2482 let dmx = open_demux_from_bytes(bytes);
2483 let md: std::collections::HashMap<String, String> =
2484 dmx.metadata().iter().cloned().collect();
2485 assert_eq!(md.get("wav:smpl.midi_unity_note"), Some(&"64".to_string()));
2486 assert_eq!(md.get("wav:inst.unshifted_note"), Some(&"64".to_string()));
2487 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
2488 }
2489
2490 fn wav_with_plst(plst_body: &[u8]) -> Vec<u8> {
2496 let mut buf = Vec::new();
2497 buf.extend_from_slice(b"RIFF");
2498 buf.extend_from_slice(&0u32.to_le_bytes());
2499 buf.extend_from_slice(b"WAVE");
2500 buf.extend_from_slice(b"fmt ");
2502 buf.extend_from_slice(&16u32.to_le_bytes());
2503 buf.extend_from_slice(&FMT_PCM.to_le_bytes());
2504 buf.extend_from_slice(&1u16.to_le_bytes());
2505 buf.extend_from_slice(&8_000u32.to_le_bytes());
2506 buf.extend_from_slice(&16_000u32.to_le_bytes());
2507 buf.extend_from_slice(&2u16.to_le_bytes());
2508 buf.extend_from_slice(&16u16.to_le_bytes());
2509 buf.extend_from_slice(b"plst");
2511 buf.extend_from_slice(&(plst_body.len() as u32).to_le_bytes());
2512 buf.extend_from_slice(plst_body);
2513 if plst_body.len() % 2 == 1 {
2514 buf.push(0);
2515 }
2516 buf.extend_from_slice(b"data");
2518 buf.extend_from_slice(&0u32.to_le_bytes());
2519 buf
2520 }
2521
2522 fn plst_segment(dw_name: u32, dw_length: u32, dw_loops: u32) -> Vec<u8> {
2525 let mut b = Vec::with_capacity(12);
2526 b.extend_from_slice(&dw_name.to_le_bytes());
2527 b.extend_from_slice(&dw_length.to_le_bytes());
2528 b.extend_from_slice(&dw_loops.to_le_bytes());
2529 b
2530 }
2531
2532 #[test]
2537 fn plst_full_metadata() {
2538 let mut plst_body = Vec::new();
2539 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);
2545 let dmx = open_demux_from_bytes(bytes);
2546 let md: std::collections::HashMap<String, String> =
2547 dmx.metadata().iter().cloned().collect();
2548
2549 assert_eq!(md.get("wav:plst.count"), Some(&"3".to_string()));
2550 assert_eq!(md.get("wav:plst.0.cue_id"), Some(&"1".to_string()));
2551 assert_eq!(md.get("wav:plst.0.length"), Some(&"4410".to_string()));
2552 assert_eq!(md.get("wav:plst.0.loops"), Some(&"1".to_string()));
2553 assert_eq!(md.get("wav:plst.1.cue_id"), Some(&"2".to_string()));
2554 assert_eq!(md.get("wav:plst.1.length"), Some(&"8820".to_string()));
2555 assert_eq!(md.get("wav:plst.1.loops"), Some(&"2".to_string()));
2556 assert_eq!(md.get("wav:plst.2.cue_id"), Some(&"1".to_string()));
2557 assert_eq!(md.get("wav:plst.2.length"), Some(&"4410".to_string()));
2558 assert_eq!(md.get("wav:plst.2.loops"), Some(&"1".to_string()));
2559 }
2560
2561 #[test]
2566 fn plst_truncated_count_is_clamped() {
2567 let mut plst_body = Vec::new();
2569 plst_body.extend_from_slice(&10u32.to_le_bytes());
2570 plst_body.extend(plst_segment(42, 1000, 1));
2571 let bytes = wav_with_plst(&plst_body);
2572 let dmx = open_demux_from_bytes(bytes);
2573 let md: std::collections::HashMap<String, String> =
2574 dmx.metadata().iter().cloned().collect();
2575 assert_eq!(md.get("wav:plst.count"), Some(&"1".to_string()));
2576 assert_eq!(md.get("wav:plst.0.cue_id"), Some(&"42".to_string()));
2577 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
2579 }
2580
2581 #[test]
2585 fn plst_truncated_header_is_opaque() {
2586 let plst_body = vec![0u8, 0]; let bytes = wav_with_plst(&plst_body);
2588 let dmx = open_demux_from_bytes(bytes);
2589 let md: std::collections::HashMap<String, String> =
2590 dmx.metadata().iter().cloned().collect();
2591 assert!(md.keys().all(|k| !k.starts_with("wav:plst.")));
2592 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
2593 }
2594
2595 #[test]
2598 fn plst_zero_segments() {
2599 let plst_body = 0u32.to_le_bytes().to_vec();
2600 let bytes = wav_with_plst(&plst_body);
2601 let dmx = open_demux_from_bytes(bytes);
2602 let md: std::collections::HashMap<String, String> =
2603 dmx.metadata().iter().cloned().collect();
2604 assert_eq!(md.get("wav:plst.count"), Some(&"0".to_string()));
2605 assert!(md
2606 .keys()
2607 .all(|k| !k.starts_with("wav:plst.") || k == "wav:plst.count"));
2608 }
2609
2610 #[test]
2613 fn plst_odd_body_padding() {
2614 let mut plst_body = Vec::new();
2616 plst_body.extend_from_slice(&1u32.to_le_bytes());
2617 plst_body.extend(plst_segment(5, 100, 1));
2618 plst_body.push(0xAA);
2619 let bytes = wav_with_plst(&plst_body);
2620 let dmx = open_demux_from_bytes(bytes);
2621 let md: std::collections::HashMap<String, String> =
2622 dmx.metadata().iter().cloned().collect();
2623 assert_eq!(md.get("wav:plst.count"), Some(&"1".to_string()));
2624 assert_eq!(md.get("wav:plst.0.cue_id"), Some(&"5".to_string()));
2625 assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
2626 }
2627
2628 #[test]
2633 fn guid_canonical_text() {
2634 assert_eq!(fmt_guid(&GUID_PCM), "00000001-0000-0010-8000-00AA00389B71");
2635 assert_eq!(
2636 fmt_guid(&GUID_IEEE_FLOAT),
2637 "00000003-0000-0010-8000-00AA00389B71"
2638 );
2639 assert_eq!(fmt_guid(&GUID_ALAW), "00000006-0000-0010-8000-00AA00389B71");
2640 assert_eq!(
2641 fmt_guid(&GUID_MULAW),
2642 "00000007-0000-0010-8000-00AA00389B71"
2643 );
2644 }
2645}