1use std::fs::File;
18use std::path::Path;
19use std::sync::Arc;
20
21use color_eyre::Result;
22use color_eyre::eyre::eyre;
23use memmap2::Mmap;
24use polars::prelude::*;
25
26pub(crate) const READER: crate::readers::Reader = crate::readers::Reader {
28 scan,
29 signatures: &[crate::readers::Signature {
30 says: |head, _| looks_like_audio(head),
31 kind: crate::readers::Kind::Magic,
32 trusted: crate::readers::EVERYWHERE,
33 }],
34 ..crate::readers::BASE
35};
36
37const MAX_CHANNELS: u16 = 1024;
39const MAX_CHUNKS: usize = 1 << 16;
42const MAX_MARKERS: usize = 100_000;
44const MAX_TEXT: usize = 1 << 20;
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum Container {
50 Wav,
52 Rf64,
54 Aiff,
55 Aifc,
57}
58
59impl Container {
60 pub fn label(self) -> &'static str {
61 match self {
62 Container::Wav => "WAV",
63 Container::Rf64 => "RF64",
64 Container::Aiff => "AIFF",
65 Container::Aifc => "AIFF-C",
66 }
67 }
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum Sample {
73 U8,
75 I8,
76 I16,
77 I24,
78 I32,
79 F32,
80 F64,
81}
82
83impl Sample {
84 pub fn bytes(self) -> usize {
86 match self {
87 Sample::U8 | Sample::I8 => 1,
88 Sample::I16 => 2,
89 Sample::I24 => 3,
90 Sample::I32 | Sample::F32 => 4,
91 Sample::F64 => 8,
92 }
93 }
94
95 pub fn is_float(self) -> bool {
96 matches!(self, Sample::F32 | Sample::F64)
97 }
98
99 pub fn dtype(self, normalize: bool) -> DataType {
102 match self {
103 Sample::F32 => DataType::Float32,
104 Sample::F64 => DataType::Float64,
105 _ if normalize => DataType::Float32,
106 Sample::U8 | Sample::I8 => DataType::Int8,
107 Sample::I16 => DataType::Int16,
108 Sample::I24 | Sample::I32 => DataType::Int32,
109 }
110 }
111
112 pub fn full_scale(self) -> f64 {
115 match self {
116 Sample::F32 | Sample::F64 => 1.0,
117 other => (1u64 << (other.bytes() * 8 - 1)) as f64,
118 }
119 }
120
121 pub fn label(self) -> String {
123 let kind = if self.is_float() { "float" } else { "integer" };
124 format!("{}-bit {kind}", self.bytes() * 8)
125 }
126}
127
128#[derive(Debug, Clone, PartialEq)]
130pub struct Marker {
131 pub id: u32,
132 pub sample: u64,
134 pub label: String,
135 pub length: Option<u64>,
137}
138
139#[derive(Debug, Clone, PartialEq)]
142pub struct AudioHeader {
143 pub container: Container,
144 pub broadcast: bool,
146 pub encoding: String,
148 pub sample: Sample,
149 pub big_endian: bool,
150 pub channels: u16,
151 pub sample_rate: f64,
152 pub valid_bits: u16,
154 pub frame_bytes: usize,
156 pub channel_names: Vec<String>,
158 pub data_offset: u64,
160 pub data_declared: Option<u64>,
163 pub metadata: Vec<(String, String)>,
165 pub markers: Vec<Marker>,
166}
167
168impl AudioHeader {
169 pub fn data_len(&self, file_len: u64) -> u64 {
172 let available = file_len.saturating_sub(self.data_offset);
173 self.data_declared
174 .map_or(available, |declared| declared.min(available))
175 }
176
177 pub fn frames(&self, file_len: u64) -> u64 {
179 self.data_len(file_len) / self.frame_bytes as u64
180 }
181
182 pub fn seconds(&self, frame: u64) -> f64 {
184 frame as f64 / self.sample_rate
185 }
186}
187
188pub fn looks_like_audio(head: &[u8]) -> bool {
190 head.len() >= 12
191 && ((matches!(&head[0..4], b"RIFF" | b"RF64" | b"BW64") && &head[8..12] == b"WAVE")
192 || (&head[0..4] == b"FORM" && matches!(&head[8..12], b"AIFF" | b"AIFC")))
193}
194
195pub fn read_header(bytes: &[u8]) -> Result<AudioHeader> {
197 if bytes.len() < 12 {
198 return Err(eyre!("Not an audio file: too short for a header"));
199 }
200 match (&bytes[0..4], &bytes[8..12]) {
201 (b"RIFF" | b"RF64" | b"BW64", b"WAVE") => read_wave(bytes),
202 (b"FORM", b"AIFF" | b"AIFC") => read_aiff(bytes),
203 (b"RIFX", _) => Err(eyre!("Big-endian WAV (RIFX) is not supported")),
204 _ => Err(eyre!("Not a WAV or AIFF file")),
205 }
206}
207
208fn le16(b: &[u8], at: usize) -> u16 {
209 u16::from_le_bytes([b[at], b[at + 1]])
210}
211fn le32(b: &[u8], at: usize) -> u32 {
212 u32::from_le_bytes([b[at], b[at + 1], b[at + 2], b[at + 3]])
213}
214fn le64(b: &[u8], at: usize) -> u64 {
215 let mut a = [0u8; 8];
216 a.copy_from_slice(&b[at..at + 8]);
217 u64::from_le_bytes(a)
218}
219fn be16(b: &[u8], at: usize) -> u16 {
220 u16::from_be_bytes([b[at], b[at + 1]])
221}
222fn be32(b: &[u8], at: usize) -> u32 {
223 u32::from_be_bytes([b[at], b[at + 1], b[at + 2], b[at + 3]])
224}
225
226fn text(raw: &[u8]) -> String {
229 let raw = &raw[..raw.len().min(MAX_TEXT)];
230 let end = raw.iter().position(|&b| b == 0).unwrap_or(raw.len());
231 String::from_utf8_lossy(&raw[..end])
232 .chars()
233 .filter(|c| !c.is_control() || matches!(c, '\n' | '\t'))
234 .collect::<String>()
235 .replace("\r\n", "\n")
236 .trim()
237 .to_string()
238}
239
240const SPEAKERS: [&str; 18] = [
242 "L", "R", "C", "LFE", "BL", "BR", "FLC", "FRC", "BC", "SL", "SR", "TC", "TFL", "TFC", "TFR",
243 "TBL", "TBC", "TBR",
244];
245
246fn channel_names(channels: u16, mask: u32) -> Vec<String> {
249 let mut named = SPEAKERS
250 .iter()
251 .enumerate()
252 .filter(|(bit, _)| mask & (1 << bit) != 0)
253 .map(|(_, name)| name.to_string());
254 (1..=channels)
255 .map(|n| named.next().unwrap_or_else(|| format!("ch{n}")))
256 .collect()
257}
258
259const SUBTYPE_TAIL: [u8; 14] = [
261 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71,
262];
263
264fn tag_name(tag: u16) -> String {
266 match tag {
267 0x0002 => "ADPCM".into(),
268 0x0006 => "A-law".into(),
269 0x0007 => "mu-law".into(),
270 0x0011 => "IMA ADPCM".into(),
271 0x0050 | 0x0055 => "MPEG".into(),
272 other => format!("format tag 0x{other:04X}"),
273 }
274}
275
276struct Fmt {
277 sample: Sample,
278 encoding: String,
279 channels: u16,
280 rate: u32,
281 block_align: u16,
282 valid_bits: u16,
283 mask: u32,
284}
285
286fn read_fmt(body: &[u8]) -> Result<Fmt> {
287 if body.len() < 16 {
288 return Err(eyre!(
289 "WAV fmt chunk is {} bytes; at least 16 needed",
290 body.len()
291 ));
292 }
293 let mut tag = le16(body, 0);
294 let channels = le16(body, 2);
295 let rate = le32(body, 4);
296 let block_align = le16(body, 12);
297 let bits = le16(body, 14);
298 let mut valid_bits = bits;
299 let mut mask = 0;
300 let mut encoding = String::new();
301 if tag == 0xFFFE {
302 if body.len() < 40 {
303 return Err(eyre!(
304 "WAV extensible fmt chunk is {} bytes; 40 needed",
305 body.len()
306 ));
307 }
308 valid_bits = le16(body, 18);
309 mask = le32(body, 20);
310 if body[26..40] != SUBTYPE_TAIL {
311 return Err(eyre!("WAV extensible subformat is not PCM or float"));
312 }
313 tag = le16(body, 24);
314 encoding.push_str("extensible ");
315 }
316 let container = match block_align.checked_rem(channels) {
320 Some(0) if tag == 1 && (9..=32).contains(&bits) => {
321 (block_align / channels).clamp(bits.div_ceil(8), 4)
322 }
323 _ => bits.div_ceil(8),
324 };
325 let sample = match (tag, container) {
326 (1, 1) => Sample::U8,
327 (1, 2) => Sample::I16,
328 (1, 3) => Sample::I24,
329 (1, 4) => Sample::I32,
330 (3, 4) if bits == 32 => Sample::F32,
331 (3, 8) if bits == 64 => Sample::F64,
332 (1 | 3, _) => {
333 return Err(eyre!("WAV with {bits}-bit samples is not supported"));
334 }
335 (other, _) => {
336 return Err(eyre!(
337 "WAV {} audio is not supported; datui reads PCM and float",
338 tag_name(other)
339 ));
340 }
341 };
342 encoding.push_str(if tag == 3 { "IEEE float" } else { "PCM" });
343 if valid_bits == 0 || valid_bits > bits {
344 valid_bits = bits;
345 }
346 Ok(Fmt {
347 sample,
348 encoding,
349 channels,
350 rate,
351 block_align,
352 valid_bits,
353 mask,
354 })
355}
356
357fn frame_bytes(channels: u16, sample: Sample, block_align: Option<u16>) -> Result<usize> {
359 if channels == 0 {
360 return Err(eyre!("Audio header says 0 channels"));
361 }
362 if channels > MAX_CHANNELS {
363 return Err(eyre!(
364 "Audio header says {channels} channels; datui reads up to {MAX_CHANNELS}"
365 ));
366 }
367 let packed = channels as usize * sample.bytes();
368 match block_align {
369 Some(align) if (align as usize) < packed => Err(eyre!(
371 "Audio header's frame size ({align} bytes) cannot hold {channels} channels of {}",
372 sample.label()
373 )),
374 Some(align) => Ok(align as usize),
375 None => Ok(packed),
376 }
377}
378
379fn check_rate(rate: f64) -> Result<f64> {
380 if rate.is_finite() && (1.0..=1e9).contains(&rate) {
381 Ok(rate)
382 } else {
383 Err(eyre!("Audio header's sample rate ({rate}) is not usable"))
384 }
385}
386
387fn chunks(bytes: &[u8], big_endian: bool) -> impl Iterator<Item = ([u8; 4], u64, u64)> + '_ {
393 let len = bytes.len() as u64;
394 let mut pos: u64 = 12;
395 let mut seen = 0;
396 std::iter::from_fn(move || {
397 if seen >= MAX_CHUNKS || pos.checked_add(8)? > len {
398 return None;
399 }
400 seen += 1;
401 let at = pos as usize;
402 let mut id = [0u8; 4];
403 id.copy_from_slice(&bytes[at..at + 4]);
404 let size = if big_endian {
405 be32(bytes, at + 4)
406 } else {
407 le32(bytes, at + 4)
408 } as u64;
409 let body = pos + 8;
410 pos = body.saturating_add(size).saturating_add(size & 1);
413 Some((id, body, size))
414 })
415}
416
417fn body_of(bytes: &[u8], start: u64, size: u64) -> Option<&[u8]> {
419 let end = start.checked_add(size)?;
420 bytes.get(start as usize..usize::try_from(end).ok()?)
421}
422
423fn read_wave(bytes: &[u8]) -> Result<AudioHeader> {
424 let len = bytes.len() as u64;
425 let container = match &bytes[0..4] {
426 b"RIFF" => Container::Wav,
427 _ => Container::Rf64,
428 };
429 let mut fmt = None;
430 let mut ds64_data = None;
431 let mut data: Option<(u64, Option<u64>)> = None;
432 let mut metadata = Vec::new();
433 let mut broadcast = false;
434 let mut cues: Vec<(u32, u64)> = Vec::new();
435 let mut labels: Vec<(u32, String)> = Vec::new();
436 let mut lengths: Vec<(u32, u64)> = Vec::new();
437 for (id, start, size) in chunks(bytes, false) {
438 if &id == b"data" {
439 let stated = match (container, size) {
440 (Container::Rf64, 0xFFFF_FFFF) => ds64_data,
441 (_, 0 | 0xFFFF_FFFF) => None,
442 (Container::Wav, size) => Some(unwrapped_size(size, len - start)),
443 (_, size) => Some(size),
444 };
445 let runs_on = stated.is_none_or(|s| start.saturating_add(s) >= len);
448 data = Some((start, stated.filter(|&s| s > 0)));
449 if runs_on {
450 break;
451 }
452 continue;
453 }
454 let Some(body) = body_of(bytes, start, size) else {
455 break;
458 };
459 match &id {
460 b"fmt " => fmt = Some(read_fmt(body)?),
461 b"ds64" if body.len() >= 24 => ds64_data = Some(le64(body, 8)),
462 b"bext" => {
463 broadcast = true;
464 read_bext(body, &mut metadata);
465 }
466 b"iXML" => read_ixml(body, &mut metadata),
467 b"cue " => read_cue(body, &mut cues),
468 b"LIST" if body.len() >= 4 => match &body[0..4] {
469 b"adtl" => read_adtl(&body[4..], &mut labels, &mut lengths),
470 b"INFO" => read_info(&body[4..], &mut metadata),
471 _ => {}
472 },
473 _ => {}
474 }
475 }
476 let fmt = fmt.ok_or_else(|| eyre!("WAV file has no fmt chunk"))?;
477 if let Some((_, value)) = metadata
480 .iter_mut()
481 .find(|(key, _)| key == "bext.time_reference")
482 && let Some(samples) = value
483 .strip_suffix(" samples")
484 .and_then(|n| n.parse::<u64>().ok())
485 && fmt.rate > 0
486 {
487 let ms = (samples as u128 * 1000 / fmt.rate as u128) as u64;
488 *value = format!(
489 "{:02}:{:02}:{:02}.{:03} ({samples} samples)",
490 ms / 3_600_000,
491 ms / 60_000 % 60,
492 ms / 1000 % 60,
493 ms % 1000
494 );
495 }
496 let (data_offset, data_declared) = data.ok_or_else(|| eyre!("WAV file has no data chunk"))?;
497 let frame_bytes = frame_bytes(fmt.channels, fmt.sample, Some(fmt.block_align))?;
498 let sample_rate = check_rate(fmt.rate as f64)?;
499 let markers = cues
500 .into_iter()
501 .map(|(id, sample)| Marker {
502 id,
503 sample,
504 label: labels
505 .iter()
506 .find(|(l, _)| *l == id)
507 .map(|(_, s)| s.clone())
508 .unwrap_or_default(),
509 length: lengths.iter().find(|(l, _)| *l == id).map(|(_, n)| *n),
510 })
511 .collect();
512 Ok(AudioHeader {
513 container,
514 broadcast,
515 encoding: fmt.encoding,
516 sample: fmt.sample,
517 big_endian: false,
518 channels: fmt.channels,
519 sample_rate,
520 valid_bits: fmt.valid_bits,
521 frame_bytes,
522 channel_names: channel_names(fmt.channels, fmt.mask),
523 data_offset,
524 data_declared,
525 metadata,
526 markers,
527 })
528}
529
530fn unwrapped_size(stated: u64, available: u64) -> u64 {
534 if available <= u32::MAX as u64 || stated > available {
535 return stated;
536 }
537 stated + ((available - stated) >> 32 << 32)
538}
539
540fn read_bext(body: &[u8], metadata: &mut Vec<(String, String)>) {
542 let field = |from: usize, to: usize| body.get(from..to).map(text).unwrap_or_default();
543 let mut put = |key: &str, value: String| {
544 if !value.is_empty() {
545 metadata.push((key.to_string(), value));
546 }
547 };
548 put("bext.description", field(0, 256));
549 put("bext.originator", field(256, 288));
550 put("bext.originator_reference", field(288, 320));
551 let date = field(320, 330);
552 let time = field(330, 338);
553 put(
554 "bext.origination",
555 format!("{date} {time}").trim().to_string(),
556 );
557 if body.len() >= 346 {
558 let samples = le32(body, 338) as u64 | ((le32(body, 342) as u64) << 32);
559 put("bext.time_reference", format!("{samples} samples"));
560 }
561 if body.len() >= 348 {
562 put("bext.version", le16(body, 346).to_string());
563 }
564 if body.len() > 602 {
565 put("bext.coding_history", field(602, body.len()));
566 }
567}
568
569fn read_ixml(body: &[u8], metadata: &mut Vec<(String, String)>) {
572 let doc = text(body);
573 for tag in ["PROJECT", "SCENE", "TAKE", "TAPE", "NOTE"] {
574 let open = format!("<{tag}>");
575 let close = format!("</{tag}>");
576 if let Some(from) = doc.find(&open).map(|i| i + open.len())
577 && let Some(len) = doc[from..].find(&close)
578 {
579 let value = doc[from..from + len].trim();
580 if !value.is_empty() {
581 metadata.push((format!("ixml.{}", tag.to_lowercase()), value.to_string()));
582 }
583 }
584 }
585 if !doc.is_empty() {
586 metadata.push(("ixml".to_string(), doc));
587 }
588}
589
590fn read_cue(body: &[u8], cues: &mut Vec<(u32, u64)>) {
591 if body.len() < 4 {
592 return;
593 }
594 let fits = (body.len() - 4) / 24;
596 let count = (le32(body, 0) as usize).min(fits).min(MAX_MARKERS);
597 for i in 0..count {
598 let at = 4 + i * 24;
599 cues.push((le32(body, at), le32(body, at + 20) as u64));
600 }
601}
602
603fn sub_chunks(body: &[u8]) -> impl Iterator<Item = ([u8; 4], &[u8])> + '_ {
605 let mut pos = 0usize;
606 std::iter::from_fn(move || {
607 let head = body.get(pos..pos.checked_add(8)?)?;
608 let mut id = [0u8; 4];
609 id.copy_from_slice(&head[0..4]);
610 let size = le32(head, 4) as usize;
611 let start = pos + 8;
612 let sub = body.get(start..start.checked_add(size)?)?;
613 pos = start + size + (size & 1);
614 Some((id, sub))
615 })
616}
617
618fn read_adtl(body: &[u8], labels: &mut Vec<(u32, String)>, lengths: &mut Vec<(u32, u64)>) {
619 for (id, sub) in sub_chunks(body).take(MAX_MARKERS) {
620 if sub.len() < 4 {
621 continue;
622 }
623 let cue = le32(sub, 0);
624 match &id {
625 b"labl" => labels.push((cue, text(&sub[4..]))),
626 b"ltxt" if sub.len() >= 8 => lengths.push((cue, le32(sub, 4) as u64)),
627 _ => {}
628 }
629 }
630}
631
632fn read_info(body: &[u8], metadata: &mut Vec<(String, String)>) {
633 for (id, sub) in sub_chunks(body).take(256) {
634 let key = match &id {
635 b"INAM" => "title",
636 b"IART" => "artist",
637 b"ICMT" => "comment",
638 b"ICRD" => "date",
639 b"ISFT" => "software",
640 b"IENG" => "engineer",
641 b"ICOP" => "copyright",
642 b"IPRD" => "product",
643 b"IGNR" => "genre",
644 _ => continue,
645 };
646 let value = text(sub);
647 if !value.is_empty() {
648 metadata.push((format!("info.{key}"), value));
649 }
650 }
651}
652
653fn extended_to_f64(b: &[u8]) -> f64 {
655 let sign_exp = u16::from_be_bytes([b[0], b[1]]);
656 let mut m = [0u8; 8];
657 m.copy_from_slice(&b[2..10]);
658 let mantissa = u64::from_be_bytes(m);
659 if mantissa == 0 {
660 return 0.0;
661 }
662 let exponent = (sign_exp & 0x7FFF) as i32 - 16383 - 63;
663 let value = mantissa as f64 * 2f64.powi(exponent);
664 if sign_exp & 0x8000 != 0 {
665 -value
666 } else {
667 value
668 }
669}
670
671fn pstring(b: &[u8]) -> Option<(String, usize)> {
674 let n = *b.first()? as usize;
675 let raw = b.get(1..1 + n)?;
676 let taken = 1 + n + ((1 + n) & 1);
677 Some((text(raw), taken))
678}
679
680fn read_aiff(bytes: &[u8]) -> Result<AudioHeader> {
681 let len = bytes.len() as u64;
682 let aifc = &bytes[8..12] == b"AIFC";
683 let mut comm = None;
684 let mut data: Option<(u64, Option<u64>)> = None;
685 let mut metadata = Vec::new();
686 let mut markers = Vec::new();
687 for (id, start, size) in chunks(bytes, true) {
688 if &id == b"SSND" {
689 let Some(head) = body_of(bytes, start, 8) else {
690 break;
691 };
692 let offset = be32(head, 0) as u64;
693 let data_start = start + 8 + offset;
694 let stated = (size > 0)
696 .then(|| size.checked_sub(8 + offset))
697 .flatten()
698 .filter(|&s| s > 0);
699 let runs_on = stated.is_none_or(|s| data_start.saturating_add(s) >= len);
700 data = Some((data_start, stated));
701 if runs_on {
702 break;
703 }
704 continue;
705 }
706 let Some(body) = body_of(bytes, start, size) else {
707 break;
708 };
709 match &id {
710 b"COMM" => comm = Some(read_comm(body, aifc)?),
711 b"MARK" if body.len() >= 2 => {
712 let count = (be16(body, 0) as usize).min(MAX_MARKERS);
713 let mut at = 2;
714 for _ in 0..count {
715 let Some(head) = body.get(at..at + 6) else {
716 break;
717 };
718 let id = be16(head, 0) as u32;
719 let sample = be32(head, 2) as u64;
720 let Some((label, taken)) = body.get(at + 6..).and_then(pstring) else {
721 break;
722 };
723 markers.push(Marker {
724 id,
725 sample,
726 label,
727 length: None,
728 });
729 at += 6 + taken;
730 }
731 }
732 b"NAME" | b"AUTH" | b"(c) " | b"ANNO" => {
733 let key = match &id {
734 b"NAME" => "name",
735 b"AUTH" => "author",
736 b"(c) " => "copyright",
737 _ => "annotation",
738 };
739 let value = text(body);
740 if !value.is_empty() {
741 metadata.push((key.to_string(), value));
742 }
743 }
744 _ => {}
745 }
746 }
747 let comm = comm.ok_or_else(|| eyre!("AIFF file has no COMM chunk"))?;
748 let (data_offset, mut data_declared) =
749 data.ok_or_else(|| eyre!("AIFF file has no SSND chunk"))?;
750 let frame_bytes = frame_bytes(comm.channels, comm.sample, None)?;
751 if comm.frames > 0 {
753 let by_comm = comm.frames.saturating_mul(frame_bytes as u64);
754 data_declared = Some(data_declared.map_or(by_comm, |d| d.min(by_comm)));
755 }
756 Ok(AudioHeader {
757 container: if aifc {
758 Container::Aifc
759 } else {
760 Container::Aiff
761 },
762 broadcast: false,
763 encoding: comm.encoding,
764 sample: comm.sample,
765 big_endian: comm.big_endian,
766 channels: comm.channels,
767 sample_rate: comm.rate,
768 valid_bits: comm.valid_bits,
769 frame_bytes,
770 channel_names: channel_names(comm.channels, 0),
771 data_offset,
772 data_declared,
773 metadata,
774 markers,
775 })
776}
777
778struct Comm {
779 channels: u16,
780 frames: u64,
781 sample: Sample,
782 big_endian: bool,
783 rate: f64,
784 valid_bits: u16,
785 encoding: String,
786}
787
788fn read_comm(body: &[u8], aifc: bool) -> Result<Comm> {
789 if body.len() < 18 {
790 return Err(eyre!("AIFF COMM chunk is {} bytes; 18 needed", body.len()));
791 }
792 let channels = be16(body, 0);
793 let frames = be32(body, 2) as u64;
794 let bits = be16(body, 6);
795 let rate = check_rate(extended_to_f64(&body[8..18]))?;
796 let code: [u8; 4] = match body.get(18..22) {
797 Some(c) if aifc => [c[0], c[1], c[2], c[3]],
798 _ => *b"NONE",
799 };
800 let int = |bits: u16| match bits.div_ceil(8) {
801 1 => Some(Sample::I8),
802 2 => Some(Sample::I16),
803 3 => Some(Sample::I24),
804 4 => Some(Sample::I32),
805 _ => None,
806 };
807 let (sample, big_endian) = match &code {
808 b"NONE" | b"twos" => (int(bits), true),
809 b"sowt" => (int(bits), false),
810 b"raw " if bits <= 8 => (Some(Sample::U8), true),
811 b"in24" => (Some(Sample::I24), true),
812 b"in32" => (Some(Sample::I32), true),
813 b"23ni" => (Some(Sample::I24), false),
814 b"fl32" | b"FL32" => (Some(Sample::F32), true),
815 b"fl64" | b"FL64" => (Some(Sample::F64), true),
816 other => {
817 return Err(eyre!(
818 "AIFF-C compression \"{}\" is not supported; datui reads uncompressed audio",
819 String::from_utf8_lossy(other)
820 ));
821 }
822 };
823 let sample = sample.ok_or_else(|| eyre!("AIFF with {bits}-bit samples is not supported"))?;
824 let encoding = match &code {
825 b"NONE" | b"twos" => "PCM".to_string(),
826 b"sowt" => "PCM, little-endian".to_string(),
827 b"fl32" | b"FL32" | b"fl64" | b"FL64" => "IEEE float".to_string(),
828 other => format!("PCM ({})", String::from_utf8_lossy(other).trim()),
829 };
830 let container_bits = (sample.bytes() * 8) as u16;
831 let valid_bits = if sample.is_float() || bits == 0 || bits > container_bits {
832 container_bits
833 } else {
834 bits
835 };
836 Ok(Comm {
837 channels,
838 frames,
839 sample,
840 big_endian,
841 rate,
842 valid_bits,
843 encoding,
844 })
845}
846
847pub struct AudioSource {
849 file: Option<File>,
851 map: Mmap,
852 header: AudioHeader,
853 frames: u64,
854 normalize: bool,
856}
857
858impl crate::pushdown::Windowed for AudioSource {
861 fn window(&self, start: usize, len: usize) -> PolarsResult<LazyFrame> {
862 Ok(AudioSource::window(self, start as u64, len as u64, None)?.lazy())
863 }
864}
865
866impl std::fmt::Debug for AudioSource {
867 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
868 f.debug_struct("AudioSource")
869 .field("header", &self.header)
870 .field("frames", &self.frames)
871 .field("normalize", &self.normalize)
872 .finish()
873 }
874}
875
876pub const FRAME: &str = "frame";
878pub const SECONDS: &str = "seconds";
882const MAX_FRAMES: u64 = crate::row_index::MAX_ROWS as u64;
884
885impl AudioSource {
886 pub fn open(path: &Path, normalize: bool) -> Result<Self> {
887 let file = File::open(path)?;
888 let map = unsafe { Mmap::map(&file)? };
892 let header = read_header(&map)?;
893 let frames = header.frames(map.len() as u64).min(MAX_FRAMES);
894 Ok(Self {
895 file: Some(file),
896 map,
897 header,
898 frames,
899 normalize,
900 })
901 }
902
903 pub fn from_bytes(bytes: &[u8], normalize: bool) -> Result<Self> {
906 let header = read_header(bytes)?;
907 let mut copy = memmap2::MmapMut::map_anon(bytes.len())?;
908 copy.copy_from_slice(bytes);
909 let map = copy.make_read_only()?;
910 let frames = header.frames(map.len() as u64).min(MAX_FRAMES);
911 Ok(Self {
912 file: None,
913 map,
914 header,
915 frames,
916 normalize,
917 })
918 }
919
920 pub fn header(&self) -> &AudioHeader {
921 &self.header
922 }
923
924 pub fn frames(&self) -> u64 {
925 self.frames
926 }
927
928 pub fn normalize(&self) -> bool {
929 self.normalize
930 }
931
932 fn still_whole(&self) -> PolarsResult<()> {
936 if let Some(file) = &self.file {
937 let len = file.metadata()?.len();
938 polars_ensure!(
939 len >= self.map.len() as u64,
940 ComputeError: "the file is now {len} bytes, shorter than the {} it had when it was opened; open it again",
941 self.map.len()
942 );
943 }
944 Ok(())
945 }
946
947 pub fn seconds(&self) -> f64 {
949 self.header.seconds(self.frames)
950 }
951
952 pub fn frames_past_limit(&self) -> u64 {
954 self.header
955 .frames(self.map.len() as u64)
956 .saturating_sub(self.frames)
957 }
958
959 pub fn trailing_bytes(&self) -> u64 {
961 self.header.data_len(self.map.len() as u64) % self.header.frame_bytes as u64
962 }
963
964 pub fn cut_short(&self) -> Option<(u64, u64)> {
966 let declared = self.header.data_declared?;
967 let held = self.header.data_len(self.map.len() as u64);
968 (declared > held).then_some((declared, held))
969 }
970
971 pub fn extend(&mut self) -> Result<u64> {
976 let Some(file) = &self.file else {
977 return Ok(0);
978 };
979 let map = unsafe { Mmap::map(file)? };
981 let frames = self.header.frames(map.len() as u64).min(MAX_FRAMES);
982 let added = frames.saturating_sub(self.frames);
983 self.map = map;
984 self.frames = frames;
985 Ok(added)
986 }
987
988 pub fn schema(&self) -> Schema {
989 let mut schema = Schema::with_capacity(self.header.channels as usize + 2);
990 schema.insert(FRAME.into(), DataType::Int64);
991 schema.insert(SECONDS.into(), DataType::Float64);
992 let dtype = self.header.sample.dtype(self.normalize);
993 for name in &self.header.channel_names {
994 schema.insert(name.as_str().into(), dtype.clone());
995 }
996 schema
997 }
998
999 pub fn window(
1002 &self,
1003 start: u64,
1004 len: u64,
1005 columns: Option<&[PlSmallStr]>,
1006 ) -> PolarsResult<DataFrame> {
1007 self.still_whole()?;
1008 let start = start.min(self.frames);
1009 let n = len.min(self.frames - start);
1010 let schema = self.schema();
1011 let all: Vec<PlSmallStr>;
1012 let columns = match columns {
1013 Some(c) => c,
1014 None => {
1015 all = schema.iter_names().cloned().collect();
1016 &all
1017 }
1018 };
1019 let mut out = Vec::with_capacity(columns.len());
1020 for name in columns {
1021 let Some(which) = self.which(name) else {
1022 polars_bail!(ColumnNotFound: "{name}");
1023 };
1024 out.push(self.decode(name.clone(), which, (start..start + n).map(Some)));
1025 }
1026 DataFrame::new(n as usize, out)
1027 }
1028
1029 fn which(&self, name: &str) -> Option<Which> {
1031 match name {
1032 FRAME => Some(Which::Frame),
1033 SECONDS => Some(Which::Seconds),
1034 other => self
1035 .header
1036 .channel_names
1037 .iter()
1038 .position(|c| c == other)
1039 .map(Which::Channel),
1040 }
1041 }
1042
1043 fn sample_at(&self, frame: u64, channel: usize) -> Option<&[u8]> {
1045 if frame >= self.frames {
1046 return None;
1047 }
1048 let h = &self.header;
1049 let width = h.sample.bytes();
1050 let at = frame
1051 .checked_mul(h.frame_bytes as u64)?
1052 .checked_add(h.data_offset)?
1053 .checked_add((channel * width) as u64)?;
1054 let at = usize::try_from(at).ok()?;
1055 self.map.get(at..at.checked_add(width)?)
1056 }
1057
1058 fn decode(
1061 &self,
1062 name: PlSmallStr,
1063 which: Which,
1064 frames: impl Iterator<Item = Option<u64>>,
1065 ) -> Column {
1066 let channel = match which {
1067 Which::Frame => {
1068 return Int64Chunked::from_iter_options(name, frames.map(|f| f.map(|f| f as i64)))
1069 .into_column();
1070 }
1071 Which::Seconds => {
1072 return Float64Chunked::from_iter_options(
1073 name,
1074 frames.map(|f| f.map(|f| self.header.seconds(f))),
1075 )
1076 .into_column();
1077 }
1078 Which::Channel(c) => c,
1079 };
1080 let h = &self.header;
1081 let be = h.big_endian;
1082 let scale = 1.0 / h.sample.full_scale() as f32;
1083 let bytes = frames.map(move |f| f.and_then(|f| self.sample_at(f, channel)));
1084 let sample = h.sample;
1085 macro_rules! int_column {
1086 ($chunked:ty, $t:ty) => {{
1087 if self.normalize {
1088 Float32Chunked::from_iter_options(
1089 name,
1090 bytes.map(|b| b.map(|b| int_sample(sample, be, b) as f32 * scale)),
1091 )
1092 .into_column()
1093 } else {
1094 <$chunked>::from_iter_options(
1095 name,
1096 bytes.map(|b| b.map(|b| int_sample(sample, be, b) as $t)),
1097 )
1098 .into_column()
1099 }
1100 }};
1101 }
1102 match sample {
1103 Sample::U8 | Sample::I8 => int_column!(Int8Chunked, i8),
1104 Sample::I16 => int_column!(Int16Chunked, i16),
1105 Sample::I24 | Sample::I32 => int_column!(Int32Chunked, i32),
1106 Sample::F32 => {
1107 Float32Chunked::from_iter_options(name, bytes.map(|b| b.map(|b| f32_sample(be, b))))
1108 .into_column()
1109 }
1110 Sample::F64 => {
1111 Float64Chunked::from_iter_options(name, bytes.map(|b| b.map(|b| f64_sample(be, b))))
1112 .into_column()
1113 }
1114 }
1115 }
1116
1117 pub fn lazy(self: &Arc<Self>) -> LazyFrame {
1121 crate::row_index::lazy(self)
1122 }
1123
1124 pub fn full_scale_bounds(&self) -> (f64, f64) {
1128 let h = &self.header;
1129 if h.sample.is_float() {
1130 return (-1.0, 1.0);
1131 }
1132 let container = (h.sample.bytes() * 8) as u32;
1133 let valid = (h.valid_bits as u32).clamp(1, container);
1134 let step = (1u64 << (container - valid)) as f64;
1136 let low = -h.sample.full_scale();
1137 let high = h.sample.full_scale() - step;
1138 if self.normalize {
1139 let scale = h.sample.full_scale();
1140 (low / scale, (high / scale) as f32 as f64)
1141 } else {
1142 (low, high)
1143 }
1144 }
1145
1146 pub fn signal_report(
1152 &self,
1153 stop: &dyn Fn() -> bool,
1154 ) -> PolarsResult<Option<Vec<SignalReport>>> {
1155 let h = &self.header;
1156 let channels = h.channels as usize;
1157 let (low, high) = self.full_scale_bounds();
1158 let scale = 1.0 / h.sample.full_scale() as f32;
1159 let zero_run = (h.sample_rate / 100.0).round().max(16.0) as u64;
1160 let mut reports: Vec<SignalReport> = h
1161 .channel_names
1162 .iter()
1163 .map(|name| SignalReport {
1164 channel: name.clone(),
1165 frames: self.frames,
1166 full_scale: (low, high),
1167 clip_run_min: CLIP_RUN,
1168 zero_run_min: zero_run,
1169 ..SignalReport::default()
1170 })
1171 .collect();
1172 let mut clip_len = vec![0u64; channels];
1173 let mut zero_len = vec![0u64; channels];
1174 let mut sums = vec![0f64; channels];
1175 let decode = |b: &[u8]| -> f64 {
1178 let be = h.big_endian;
1179 match h.sample {
1180 Sample::F32 => f32_sample(be, b) as f64,
1181 Sample::F64 => f64_sample(be, b),
1182 int if self.normalize => (int_sample(int, be, b) as f32 * scale) as f64,
1183 int => int_sample(int, be, b) as f64,
1184 }
1185 };
1186 let end_run =
1187 |len: &mut u64, min: u64, runs: &mut u64, within: &mut u64, longest: &mut u64| {
1188 if *len >= min {
1189 *runs += 1;
1190 *within += *len;
1191 *longest = (*longest).max(*len);
1192 }
1193 *len = 0;
1194 };
1195 for frame in 0..self.frames {
1196 if frame % (1 << 20) == 0 {
1197 if stop() {
1198 return Ok(None);
1199 }
1200 self.still_whole()?;
1201 }
1202 for c in 0..channels {
1203 let Some(bytes) = self.sample_at(frame, c) else {
1204 continue;
1205 };
1206 let v = decode(bytes);
1207 let r = &mut reports[c];
1208 if !v.is_finite() {
1209 continue;
1210 }
1211 sums[c] += v;
1212 if v <= low || v >= high {
1213 r.at_full_scale += 1;
1214 clip_len[c] += 1;
1215 } else {
1216 end_run(
1217 &mut clip_len[c],
1218 CLIP_RUN,
1219 &mut r.clip_runs,
1220 &mut r.in_clip_runs,
1221 &mut r.longest_clip,
1222 );
1223 }
1224 if v == 0.0 {
1225 zero_len[c] += 1;
1226 } else {
1227 end_run(
1228 &mut zero_len[c],
1229 zero_run,
1230 &mut r.zero_runs,
1231 &mut r.in_zero_runs,
1232 &mut r.longest_zeros,
1233 );
1234 }
1235 }
1236 }
1237 for (c, r) in reports.iter_mut().enumerate() {
1238 end_run(
1239 &mut clip_len[c],
1240 CLIP_RUN,
1241 &mut r.clip_runs,
1242 &mut r.in_clip_runs,
1243 &mut r.longest_clip,
1244 );
1245 end_run(
1246 &mut zero_len[c],
1247 zero_run,
1248 &mut r.zero_runs,
1249 &mut r.in_zero_runs,
1250 &mut r.longest_zeros,
1251 );
1252 if self.frames > 0 {
1253 r.mean = sums[c] / self.frames as f64;
1254 }
1255 }
1256 Ok(Some(reports))
1257 }
1258}
1259
1260impl crate::row_index::RowSource for AudioSource {
1261 fn height(&self) -> usize {
1262 self.frames as usize
1263 }
1264
1265 fn schema(&self) -> SchemaRef {
1266 Arc::new(AudioSource::schema(self))
1267 }
1268
1269 fn decode(&self, column: usize, index: &IdxCa) -> PolarsResult<Column> {
1270 self.still_whole()?;
1271 let which = match column {
1272 0 => Which::Frame,
1273 1 => Which::Seconds,
1274 c => Which::Channel(c - 2),
1275 };
1276 let name = self.schema().get_at_index(column).map(|(n, _)| n.clone());
1277 let name = name.ok_or_else(|| polars_err!(ColumnNotFound: "column {column}"))?;
1278 Ok(AudioSource::decode(
1279 self,
1280 name,
1281 which,
1282 index.iter().map(|f| f.map(u64::from)),
1283 ))
1284 }
1285}
1286
1287fn int_sample(sample: Sample, be: bool, b: &[u8]) -> i32 {
1290 match sample {
1291 Sample::U8 => (b[0] ^ 0x80) as i8 as i32,
1292 Sample::I8 => b[0] as i8 as i32,
1293 Sample::I16 => {
1294 let a = [b[0], b[1]];
1295 (if be {
1296 i16::from_be_bytes(a)
1297 } else {
1298 i16::from_le_bytes(a)
1299 }) as i32
1300 }
1301 Sample::I24 => {
1302 let a = if be {
1303 [b[2], b[1], b[0], 0]
1304 } else {
1305 [b[0], b[1], b[2], 0]
1306 };
1307 i32::from_le_bytes(a) << 8 >> 8
1309 }
1310 Sample::I32 => {
1311 let a = [b[0], b[1], b[2], b[3]];
1312 if be {
1313 i32::from_be_bytes(a)
1314 } else {
1315 i32::from_le_bytes(a)
1316 }
1317 }
1318 Sample::F32 | Sample::F64 => 0,
1319 }
1320}
1321
1322fn f32_sample(be: bool, b: &[u8]) -> f32 {
1323 let a = [b[0], b[1], b[2], b[3]];
1324 if be {
1325 f32::from_be_bytes(a)
1326 } else {
1327 f32::from_le_bytes(a)
1328 }
1329}
1330
1331fn f64_sample(be: bool, b: &[u8]) -> f64 {
1332 let mut a = [0u8; 8];
1333 a.copy_from_slice(&b[..8]);
1334 if be {
1335 f64::from_be_bytes(a)
1336 } else {
1337 f64::from_le_bytes(a)
1338 }
1339}
1340
1341pub const CLIP_RUN: u64 = 3;
1344
1345#[derive(Debug, Clone, Default, PartialEq)]
1347pub struct SignalReport {
1348 pub channel: String,
1349 pub frames: u64,
1350 pub full_scale: (f64, f64),
1352 pub at_full_scale: u64,
1354 pub clip_run_min: u64,
1357 pub clip_runs: u64,
1358 pub in_clip_runs: u64,
1359 pub longest_clip: u64,
1360 pub zero_run_min: u64,
1363 pub zero_runs: u64,
1364 pub in_zero_runs: u64,
1365 pub longest_zeros: u64,
1366 pub mean: f64,
1368}
1369
1370#[derive(Debug, Clone, Copy)]
1372enum Which {
1373 Frame,
1374 Seconds,
1375 Channel(usize),
1376}
1377
1378pub(crate) fn recording(window: Arc<dyn crate::pushdown::Windowed>) -> Option<Arc<AudioSource>> {
1382 let any: Arc<dyn std::any::Any + Send + Sync> = window;
1383 any.downcast::<AudioSource>().ok()
1384}
1385
1386pub fn detail(audio: &AudioSource) -> crate::text_formats::Detail {
1389 use crate::model_files::MetaValue;
1390 use crate::widgets::info::{clock, count_of, format_bytes, group_u64};
1391 let h = audio.header();
1392 let g = crate::glyphs::get();
1393 let sep = format!(" {} ", g.middot);
1394 let mut kind = h.container.label().to_string();
1395 if h.broadcast {
1396 kind.push_str(" (Broadcast WAV)");
1397 }
1398 let rate = if h.sample_rate.fract() == 0.0 {
1399 group_u64(h.sample_rate as u64)
1400 } else {
1401 format!("{:.3}", h.sample_rate)
1402 };
1403 let mut samples = h.sample.label();
1404 if !h.sample.is_float() && (h.valid_bits as usize) < h.sample.bytes() * 8 {
1405 samples.push_str(&format!(" ({} valid)", h.valid_bits));
1406 }
1407 let mut lines = vec![
1408 format!(
1409 "{kind}{sep}{}{sep}{rate} Hz",
1410 count_of(h.channels as u64, "channel", "channels"),
1411 ),
1412 format!(
1413 "Samples: {samples}{sep}{}{}",
1414 h.encoding,
1415 if audio.normalize() && !h.sample.is_float() {
1416 format!("{sep}shown as float in [-1, 1]")
1417 } else {
1418 String::new()
1419 }
1420 ),
1421 format!(
1422 "Frames: {}{sep}Length: {}",
1423 group_u64(audio.frames()),
1424 clock(audio.seconds()),
1425 ),
1426 format!(
1427 "Data: {}",
1428 format_bytes(audio.frames() * h.frame_bytes as u64)
1429 ),
1430 ];
1431 let mut warnings = Vec::new();
1432 if let Some((declared, held)) = audio.cut_short() {
1433 warnings.push(format!(
1434 "header says {} of samples{sep}file holds {}",
1435 format_bytes(declared),
1436 format_bytes(held)
1437 ));
1438 } else if h.data_declared.is_none() && audio.frames() > 0 {
1439 lines.push(format!(
1440 "no data size in header{sep}frames counted from file size"
1441 ));
1442 }
1443 let past = audio.frames_past_limit();
1444 if past > 0 {
1445 warnings.push(format!(
1446 "last {} frames not shown: past the table limit",
1447 group_u64(past)
1448 ));
1449 }
1450 let trailing = audio.trailing_bytes();
1451 if trailing > 0 {
1452 warnings.push(format!(
1453 "{trailing} bytes after the last whole frame not shown"
1454 ));
1455 }
1456 let mut metadata: crate::model_files::Metadata = h
1457 .metadata
1458 .iter()
1459 .map(|(k, v)| (k.clone(), MetaValue::Text(v.clone())))
1460 .collect();
1461 for marker in &h.markers {
1462 let mut value = format!(
1463 "{}{sep}frame {}",
1464 clock(marker.sample as f64 / h.sample_rate),
1465 group_u64(marker.sample)
1466 );
1467 if let Some(length) = marker.length {
1468 value.push_str(&format!(
1469 "{sep}{} long",
1470 clock(length as f64 / h.sample_rate)
1471 ));
1472 }
1473 if !marker.label.is_empty() {
1474 value.push_str(&sep);
1475 value.push_str(&marker.label);
1476 }
1477 metadata.push((format!("marker {}", marker.id), MetaValue::Text(value)));
1478 }
1479 crate::text_formats::Detail {
1480 tab: crate::text_formats::tab(crate::FileFormat::Audio),
1481 lines,
1482 warnings,
1483 list_title: "Metadata",
1484 list: metadata,
1485 first: true,
1488 own_columns: true,
1489 ..Default::default()
1490 }
1491}
1492
1493fn scan(input: crate::readers::ScanIn<'_>) -> Result<crate::scan::Scan> {
1497 let source = Arc::new(AudioSource::open(input.path(), input.options.normalize)?);
1498 let lf = source.lazy();
1499 let rows = usize::try_from(source.frames()).unwrap_or(usize::MAX);
1501 input.report.opened = Some(Arc::new(crate::members::Opened {
1502 detail: Some(Arc::new(detail(&source))),
1503 window: Some((source, rows)),
1504 ..Default::default()
1505 }));
1506 Ok(lf.into())
1507}
1508
1509#[cfg(test)]
1510mod tests {
1511 use super::*;
1512 use std::io::Write;
1513
1514 fn chunk(id: &[u8; 4], body: &[u8]) -> Vec<u8> {
1515 let mut out = id.to_vec();
1516 out.extend_from_slice(&(body.len() as u32).to_le_bytes());
1517 out.extend_from_slice(body);
1518 if body.len() % 2 == 1 {
1519 out.push(0);
1520 }
1521 out
1522 }
1523
1524 fn fmt(tag: u16, channels: u16, rate: u32, bits: u16) -> Vec<u8> {
1525 let align = channels * bits.div_ceil(8);
1526 let mut b = Vec::new();
1527 b.extend_from_slice(&tag.to_le_bytes());
1528 b.extend_from_slice(&channels.to_le_bytes());
1529 b.extend_from_slice(&rate.to_le_bytes());
1530 b.extend_from_slice(&(rate * align as u32).to_le_bytes());
1531 b.extend_from_slice(&align.to_le_bytes());
1532 b.extend_from_slice(&bits.to_le_bytes());
1533 b
1534 }
1535
1536 fn wav(chunks: &[Vec<u8>]) -> Vec<u8> {
1537 let body: Vec<u8> = chunks.concat();
1538 let mut out = b"RIFF".to_vec();
1539 out.extend_from_slice(&(4 + body.len() as u32).to_le_bytes());
1540 out.extend_from_slice(b"WAVE");
1541 out.extend_from_slice(&body);
1542 out
1543 }
1544
1545 fn i16s(values: &[i16]) -> Vec<u8> {
1546 values.iter().flat_map(|v| v.to_le_bytes()).collect()
1547 }
1548
1549 fn open(bytes: &[u8], normalize: bool) -> (tempfile::NamedTempFile, AudioSource) {
1550 let mut file = tempfile::NamedTempFile::new().unwrap();
1551 file.write_all(bytes).unwrap();
1552 file.flush().unwrap();
1553 let source = AudioSource::open(file.path(), normalize).unwrap();
1554 (file, source)
1555 }
1556
1557 fn ints(df: &DataFrame, name: &str) -> Vec<i64> {
1558 df.column(name)
1559 .unwrap()
1560 .cast(&DataType::Int64)
1561 .unwrap()
1562 .i64()
1563 .unwrap()
1564 .into_no_null_iter()
1565 .collect()
1566 }
1567
1568 #[test]
1569 fn a_stereo_wav_is_frames_time_and_a_column_per_channel() {
1570 let samples = i16s(&[0, 1, -32768, 32767, 100, -100]);
1571 let bytes = wav(&[chunk(b"fmt ", &fmt(1, 2, 4, 16)), chunk(b"data", &samples)]);
1572 let (_f, source) = open(&bytes, false);
1573 assert_eq!(source.frames(), 3);
1574 let df = source.window(0, 10, None).unwrap();
1575 assert_eq!(
1576 df.get_column_names(),
1577 ["frame", "seconds", "ch1", "ch2"],
1578 "plain PCM names its channels by number"
1579 );
1580 assert_eq!(df.column("ch1").unwrap().dtype(), &DataType::Int16);
1581 assert_eq!(ints(&df, "ch1"), [0, -32768, 100]);
1582 assert_eq!(ints(&df, "ch2"), [1, 32767, -100]);
1583 let seconds: Vec<f64> = df
1585 .column("seconds")
1586 .unwrap()
1587 .f64()
1588 .unwrap()
1589 .into_no_null_iter()
1590 .collect();
1591 assert_eq!(seconds, [0.0, 0.25, 0.5]);
1592
1593 let tail = source
1595 .window(2, 5, Some(&["ch2".into(), "frame".into()]))
1596 .unwrap();
1597 assert_eq!(tail.get_column_names(), ["ch2", "frame"]);
1598 assert_eq!(
1599 (ints(&tail, "frame"), ints(&tail, "ch2")),
1600 (vec![2], vec![-100])
1601 );
1602 }
1603
1604 #[test]
1605 fn normalized_integers_are_float_in_unit_range() {
1606 let samples = i16s(&[-32768, 16384]);
1607 let bytes = wav(&[
1608 chunk(b"fmt ", &fmt(1, 1, 8000, 16)),
1609 chunk(b"data", &samples),
1610 ]);
1611 let (_f, source) = open(&bytes, true);
1612 let df = source.window(0, 2, None).unwrap();
1613 let ch: Vec<f32> = df
1614 .column("ch1")
1615 .unwrap()
1616 .f32()
1617 .unwrap()
1618 .into_no_null_iter()
1619 .collect();
1620 assert_eq!(ch, [-1.0, 0.5]);
1621 }
1622
1623 #[test]
1624 fn every_sample_width_decodes() {
1625 let bytes = wav(&[
1627 chunk(b"fmt ", &fmt(1, 1, 8000, 8)),
1628 chunk(b"data", &[0, 128, 255]),
1629 ]);
1630 let (_f, s) = open(&bytes, false);
1631 assert_eq!(ints(&s.window(0, 3, None).unwrap(), "ch1"), [-128, 0, 127]);
1632
1633 let data = [0x00, 0x00, 0x80, 0xFF, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF];
1635 let bytes = wav(&[chunk(b"fmt ", &fmt(1, 1, 8000, 24)), chunk(b"data", &data)]);
1636 let (_f, s) = open(&bytes, false);
1637 let df = s.window(0, 3, None).unwrap();
1638 assert_eq!(df.column("ch1").unwrap().dtype(), &DataType::Int32);
1639 assert_eq!(ints(&df, "ch1"), [-8_388_608, 8_388_607, -1]);
1640
1641 let data: Vec<u8> = [0.5f32, -1.0]
1643 .iter()
1644 .flat_map(|v| v.to_le_bytes())
1645 .collect();
1646 let bytes = wav(&[chunk(b"fmt ", &fmt(3, 1, 8000, 32)), chunk(b"data", &data)]);
1647 let (_f, s) = open(&bytes, true);
1648 let df = s.window(0, 2, None).unwrap();
1649 let v: Vec<f32> = df
1650 .column("ch1")
1651 .unwrap()
1652 .f32()
1653 .unwrap()
1654 .into_no_null_iter()
1655 .collect();
1656 assert_eq!(v, [0.5, -1.0], "float is never rescaled");
1657 let data: Vec<u8> = [0.25f64].iter().flat_map(|v| v.to_le_bytes()).collect();
1658 let bytes = wav(&[chunk(b"fmt ", &fmt(3, 1, 8000, 64)), chunk(b"data", &data)]);
1659 let (_f, s) = open(&bytes, false);
1660 assert_eq!(s.schema().get("ch1"), Some(&DataType::Float64));
1661
1662 let data: Vec<u8> = [i32::MIN, 7].iter().flat_map(|v| v.to_le_bytes()).collect();
1664 let bytes = wav(&[chunk(b"fmt ", &fmt(1, 1, 8000, 32)), chunk(b"data", &data)]);
1665 let (_f, s) = open(&bytes, false);
1666 assert_eq!(
1667 ints(&s.window(0, 2, None).unwrap(), "ch1"),
1668 [i32::MIN as i64, 7]
1669 );
1670 }
1671
1672 #[test]
1673 fn the_extensible_channel_mask_names_the_columns() {
1674 let mut body = fmt(0xFFFE, 4, 48000, 24);
1675 body.extend_from_slice(&22u16.to_le_bytes());
1676 body.extend_from_slice(&20u16.to_le_bytes());
1677 body.extend_from_slice(&0x0Fu32.to_le_bytes());
1679 body.extend_from_slice(&1u16.to_le_bytes());
1680 body.extend_from_slice(&SUBTYPE_TAIL);
1681 let bytes = wav(&[chunk(b"fmt ", &body), chunk(b"data", &[0; 12])]);
1682 let (_f, s) = open(&bytes, false);
1683 assert_eq!(s.header().channel_names, ["L", "R", "C", "LFE"]);
1684 assert_eq!(s.header().valid_bits, 20);
1685 assert_eq!(s.header().encoding, "extensible PCM");
1686
1687 assert_eq!(channel_names(3, 0x4), ["C", "ch2", "ch3"]);
1689 }
1690
1691 #[test]
1694 fn a_wider_container_in_the_block_alignment_is_honored() {
1695 let mut body = fmt(1, 2, 8000, 24);
1696 body[12..14].copy_from_slice(&8u16.to_le_bytes());
1697 let samples: Vec<u8> = [0x0001_0000i32 << 8, -256]
1698 .iter()
1699 .flat_map(|v| v.to_le_bytes())
1700 .collect();
1701 let bytes = wav(&[chunk(b"fmt ", &body), chunk(b"data", &samples)]);
1702 let (_f, source) = open(&bytes, false);
1703 assert_eq!(source.header().sample, Sample::I32);
1704 assert_eq!(source.header().valid_bits, 24);
1705 let df = source.window(0, 1, None).unwrap();
1706 assert_eq!(ints(&df, "ch1"), [0x0001_0000 << 8]);
1707 assert_eq!(ints(&df, "ch2"), [-256]);
1708 }
1709
1710 #[test]
1712 fn a_data_size_that_wrapped_past_4_gib_is_unwrapped() {
1713 const GIB4: u64 = 1 << 32;
1714 assert_eq!(unwrapped_size(100, 1_000), 100, "a small file as stated");
1715 assert_eq!(unwrapped_size(100, GIB4 + 100), GIB4 + 100);
1716 assert_eq!(
1717 unwrapped_size(100, 2 * GIB4 + 150),
1718 2 * GIB4 + 100,
1719 "chunks after the data are not samples"
1720 );
1721 assert_eq!(
1722 unwrapped_size(GIB4 - 2, GIB4 + 10),
1723 GIB4 - 2,
1724 "fits as stated"
1725 );
1726 }
1727
1728 #[test]
1729 fn a_recording_in_progress_is_counted_by_the_file_and_grows() {
1730 let mut bytes = wav(&[chunk(b"fmt ", &fmt(1, 1, 8000, 16))]);
1733 bytes.extend_from_slice(b"data");
1734 bytes.extend_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
1735 bytes.extend_from_slice(&i16s(&[1, 2, 3]));
1736 bytes.push(0x04);
1737 let (mut file, mut source) = open(&bytes, false);
1738 assert_eq!(source.header().data_declared, None);
1739 assert_eq!(source.frames(), 3);
1740 assert_eq!(source.trailing_bytes(), 1);
1741
1742 file.write_all(&[0x00]).unwrap();
1743 file.write_all(&i16s(&[5, 6])).unwrap();
1744 file.flush().unwrap();
1745 assert_eq!(
1746 source.extend().unwrap(),
1747 3,
1748 "the finished frame and two more"
1749 );
1750 assert_eq!(ints(&source.window(3, 3, None).unwrap(), "ch1"), [4, 5, 6]);
1751 }
1752
1753 #[test]
1756 fn a_file_cut_short_while_open_is_an_error_to_read() {
1757 let samples = i16s(&[1; 4096]);
1758 let bytes = wav(&[
1759 chunk(b"fmt ", &fmt(1, 1, 8000, 16)),
1760 chunk(b"data", &samples),
1761 ]);
1762 let (file, source) = open(&bytes, false);
1763 let source = Arc::new(source);
1764 assert!(source.window(4000, 10, None).is_ok());
1765 let cut = file.as_file().set_len(64);
1766 if cfg!(windows) {
1768 assert_eq!(cut.unwrap_err().raw_os_error(), Some(1224));
1769 assert!(source.window(4000, 10, None).is_ok());
1770 return;
1771 }
1772 cut.unwrap();
1773 let err = source.window(4000, 10, None).unwrap_err().to_string();
1774 assert!(err.contains("open it again"), "{err}");
1775 assert!(source.lazy().collect().is_err());
1776 assert!(source.signal_report(&|| false).is_err());
1777 }
1778
1779 #[test]
1780 fn a_data_size_past_the_end_is_cut_to_the_file() {
1781 let mut bytes = wav(&[chunk(b"fmt ", &fmt(1, 1, 8000, 16))]);
1782 bytes.extend_from_slice(b"data");
1783 bytes.extend_from_slice(&1000u32.to_le_bytes());
1784 bytes.extend_from_slice(&i16s(&[1, 2]));
1785 let (_f, source) = open(&bytes, false);
1786 assert_eq!(source.frames(), 2);
1787 assert_eq!(source.cut_short(), Some((1000, 4)));
1788 }
1789
1790 #[test]
1791 fn rf64_takes_its_sizes_from_ds64() {
1792 let mut ds64 = Vec::new();
1793 ds64.extend_from_slice(&0u64.to_le_bytes());
1794 ds64.extend_from_slice(&4u64.to_le_bytes());
1795 ds64.extend_from_slice(&2u64.to_le_bytes());
1796 ds64.extend_from_slice(&0u32.to_le_bytes());
1797 let mut body = chunk(b"ds64", &ds64);
1798 body.extend(chunk(b"fmt ", &fmt(1, 1, 8000, 16)));
1799 body.extend_from_slice(b"data");
1800 body.extend_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
1801 body.extend_from_slice(&i16s(&[9, 8, 7]));
1802 let mut bytes = b"RF64".to_vec();
1803 bytes.extend_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
1804 bytes.extend_from_slice(b"WAVE");
1805 bytes.extend(body);
1806 let (_f, source) = open(&bytes, false);
1807 assert_eq!(source.header().container, Container::Rf64);
1808 assert_eq!(source.frames(), 2, "ds64 says two frames");
1809 }
1810
1811 #[test]
1812 fn bext_ixml_info_and_cue_labels_are_read() {
1813 let mut bext = vec![0u8; 602];
1814 bext[..11].copy_from_slice(b"Scene notes");
1815 bext[256..262].copy_from_slice(b"Mixer1");
1816 bext[320..330].copy_from_slice(b"2026-10-02");
1817 bext[330..338].copy_from_slice(b"12:00:00");
1818 bext[338..342].copy_from_slice(&48000u32.to_le_bytes());
1819 bext.extend_from_slice(b"A=PCM,F=48000\r\n");
1820 let ixml = b"<BWFXML><PROJECT>Film</PROJECT><SCENE>12A</SCENE><TAKE>3</TAKE></BWFXML>";
1821 let mut cue = 2u32.to_le_bytes().to_vec();
1822 for (id, at) in [(1u32, 0u32), (2, 2)] {
1823 cue.extend_from_slice(&id.to_le_bytes());
1824 cue.extend_from_slice(&at.to_le_bytes());
1825 cue.extend_from_slice(b"data");
1826 cue.extend_from_slice(&[0; 8]);
1827 cue.extend_from_slice(&at.to_le_bytes());
1828 }
1829 let mut labl = 1u32.to_le_bytes().to_vec();
1830 labl.extend_from_slice(b"Intro\0");
1831 let mut ltxt = 2u32.to_le_bytes().to_vec();
1832 ltxt.extend_from_slice(&1u32.to_le_bytes());
1833 ltxt.extend_from_slice(&[0; 12]);
1834 let mut adtl = b"adtl".to_vec();
1835 adtl.extend(chunk(b"labl", &labl));
1836 adtl.extend(chunk(b"ltxt", <xt));
1837 let mut info = b"INFO".to_vec();
1838 info.extend(chunk(b"INAM", b"Take three\0"));
1839 let bytes = wav(&[
1840 chunk(b"bext", &bext),
1841 chunk(b"iXML", ixml),
1842 chunk(b"fmt ", &fmt(1, 1, 4, 16)),
1843 chunk(b"data", &i16s(&[0, 0, 0])),
1844 chunk(b"cue ", &cue),
1845 chunk(b"LIST", &adtl),
1846 chunk(b"LIST", &info),
1847 ]);
1848 let (_f, source) = open(&bytes, false);
1849 let h = source.header();
1850 assert!(h.broadcast);
1851 let meta = |k: &str| {
1852 h.metadata
1853 .iter()
1854 .find(|(key, _)| key == k)
1855 .map(|(_, v)| v.as_str())
1856 .unwrap_or_else(|| panic!("no {k}: {:?}", h.metadata))
1857 };
1858 assert_eq!(meta("bext.description"), "Scene notes");
1859 assert_eq!(meta("bext.originator"), "Mixer1");
1860 assert_eq!(meta("bext.origination"), "2026-10-02 12:00:00");
1861 assert_eq!(
1862 meta("bext.time_reference"),
1863 "03:20:00.000 (48000 samples)",
1864 "a time of day at the file's rate of 4 a second"
1865 );
1866 assert_eq!(meta("bext.coding_history"), "A=PCM,F=48000");
1867 assert_eq!(meta("ixml.scene"), "12A");
1868 assert_eq!(meta("ixml.take"), "3");
1869 assert_eq!(meta("info.title"), "Take three");
1870 assert_eq!(
1871 h.markers,
1872 [
1873 Marker {
1874 id: 1,
1875 sample: 0,
1876 label: "Intro".into(),
1877 length: None
1878 },
1879 Marker {
1880 id: 2,
1881 sample: 2,
1882 label: String::new(),
1883 length: Some(1)
1884 }
1885 ],
1886 "cue points after the data are found, labeled from adtl"
1887 );
1888 }
1889
1890 fn aiff(kind: &[u8; 4], comm: &[u8], ssnd: &[u8], extra: &[u8]) -> Vec<u8> {
1892 let mut body = kind.to_vec();
1893 let be_chunk = |id: &[u8; 4], b: &[u8]| {
1894 let mut out = id.to_vec();
1895 out.extend_from_slice(&(b.len() as u32).to_be_bytes());
1896 out.extend_from_slice(b);
1897 if b.len() % 2 == 1 {
1898 out.push(0);
1899 }
1900 out
1901 };
1902 body.extend(be_chunk(b"COMM", comm));
1903 body.extend_from_slice(extra);
1904 let mut ssnd_body = vec![0u8; 8];
1905 ssnd_body.extend_from_slice(ssnd);
1906 body.extend(be_chunk(b"SSND", &ssnd_body));
1907 let mut out = b"FORM".to_vec();
1908 out.extend_from_slice(&(body.len() as u32).to_be_bytes());
1909 out.extend(body);
1910 out
1911 }
1912
1913 const RATE_44100: [u8; 10] = [0x40, 0x0E, 0xAC, 0x44, 0, 0, 0, 0, 0, 0];
1915
1916 fn comm(channels: u16, frames: u32, bits: u16, code: Option<&[u8; 4]>) -> Vec<u8> {
1917 let mut c = channels.to_be_bytes().to_vec();
1918 c.extend_from_slice(&frames.to_be_bytes());
1919 c.extend_from_slice(&bits.to_be_bytes());
1920 c.extend_from_slice(&RATE_44100);
1921 if let Some(code) = code {
1922 c.extend_from_slice(code);
1923 c.extend_from_slice(&[0, 0]);
1924 }
1925 c
1926 }
1927
1928 #[test]
1929 fn aiff_is_big_endian_with_an_extended_rate() {
1930 let data: Vec<u8> = [1i16, -2, 300, -400]
1931 .iter()
1932 .flat_map(|v| v.to_be_bytes())
1933 .collect();
1934 let mut mark = 1u16.to_be_bytes().to_vec();
1935 mark.extend_from_slice(&7u16.to_be_bytes());
1936 mark.extend_from_slice(&1u32.to_be_bytes());
1937 mark.extend_from_slice(b"\x05Verse");
1938 let mut extra = b"MARK".to_vec();
1939 extra.extend_from_slice(&(mark.len() as u32).to_be_bytes());
1940 extra.extend(mark);
1941 let bytes = aiff(b"AIFF", &comm(2, 2, 16, None), &data, &extra);
1942 let (_f, s) = open(&bytes, false);
1943 assert_eq!(s.header().sample_rate, 44100.0);
1944 assert_eq!(s.header().container, Container::Aiff);
1945 let df = s.window(0, 2, None).unwrap();
1946 assert_eq!(ints(&df, "ch1"), [1, 300]);
1947 assert_eq!(ints(&df, "ch2"), [-2, -400]);
1948 assert_eq!(s.header().markers[0].label, "Verse");
1949 assert_eq!(s.header().markers[0].sample, 1);
1950
1951 let data: Vec<u8> = [5i16, -6].iter().flat_map(|v| v.to_le_bytes()).collect();
1953 let bytes = aiff(b"AIFC", &comm(1, 2, 16, Some(b"sowt")), &data, &[]);
1954 let (_f, s) = open(&bytes, false);
1955 assert_eq!(ints(&s.window(0, 2, None).unwrap(), "ch1"), [5, -6]);
1956 let data: Vec<u8> = [0.5f32].iter().flat_map(|v| v.to_be_bytes()).collect();
1957 let bytes = aiff(b"AIFC", &comm(1, 1, 32, Some(b"fl32")), &data, &[]);
1958 let (_f, s) = open(&bytes, false);
1959 assert_eq!(s.header().sample, Sample::F32);
1960
1961 let data: Vec<u8> = [1i16, 2, 3].iter().flat_map(|v| v.to_be_bytes()).collect();
1963 let bytes = aiff(b"AIFF", &comm(1, 2, 16, None), &data, &[]);
1964 let (_f, s) = open(&bytes, false);
1965 assert_eq!(s.frames(), 2);
1966
1967 let bytes = aiff(b"AIFC", &comm(1, 1, 16, Some(b"ima4")), &[0; 34], &[]);
1969 let err = read_header(&bytes).unwrap_err().to_string();
1970 assert!(err.contains("ima4"), "{err}");
1971 }
1972
1973 #[test]
1974 fn the_lazy_scan_pushes_projection_and_counts_without_decoding() {
1975 let samples = i16s(&[1, 2, 3, 4, 5, 6]);
1976 let bytes = wav(&[
1977 chunk(b"fmt ", &fmt(1, 2, 8000, 16)),
1978 chunk(b"data", &samples),
1979 ]);
1980 let (_f, source) = open(&bytes, false);
1981 let lf = Arc::new(source).lazy();
1982 let df = lf.clone().select([col("ch2")]).collect().unwrap();
1983 assert_eq!(ints(&df, "ch2"), [2, 4, 6]);
1984 let n = lf.clone().select([len()]).collect().unwrap();
1985 assert_eq!(n.column("len").unwrap().u32().unwrap().get(0), Some(3));
1986 let head = lf.limit(2).collect().unwrap();
1987 assert_eq!(head.height(), 2);
1988 }
1989
1990 #[test]
1993 fn a_waveform_charts_as_its_envelope_over_seconds() {
1994 let mut values = vec![0i16; 20_000];
1995 values[12_345] = 30_000;
1996 let bytes = wav(&[
1997 chunk(b"fmt ", &fmt(1, 1, 8000, 16)),
1998 chunk(b"data", &i16s(&values)),
1999 ]);
2000 let (_f, source) = open(&bytes, false);
2001 let source = Arc::new(source);
2002 let lf = source.lazy();
2003 let schema = source.schema();
2004 let result = crate::chart_data::prepare_chart_data(
2005 &lf,
2006 &schema,
2007 SECONDS,
2008 &["ch1".into()],
2009 &crate::chart_data::ChartSampling::rows(Some(1_000)),
2010 true,
2011 )
2012 .unwrap();
2013 assert_eq!(result.rows.total_rows, 20_000);
2014 assert_eq!(result.rows.envelope_steps, Some(500));
2015 let top = result.series[0]
2016 .iter()
2017 .map(|p| p.1)
2018 .fold(f64::MIN, f64::max);
2019 assert_eq!(top, 30_000.0);
2020 let last = result.series[0].last().unwrap().0;
2021 assert!(last > 2.49 && last < 2.5, "X in seconds: {last}");
2022 }
2023
2024 #[test]
2026 fn errors_name_the_file() {
2027 let no_comm = {
2028 let mut out = b"FORM".to_vec();
2029 out.extend_from_slice(&4u32.to_be_bytes());
2030 out.extend_from_slice(b"AIFF");
2031 out
2032 };
2033 crate::readers::bad_input::each_names_its_file(
2034 crate::FileFormat::Audio,
2035 &[
2036 ("short.wav", b"RIFF", "too short"),
2037 ("rifx.wav", b"RIFX\0\0\0\0WAVEfmt ", "RIFX"),
2038 ("other.wav", b"OggS\0\0\0\0\0\0\0\0", "Not a WAV or AIFF"),
2039 ("nofmt.wav", &wav(&[chunk(b"data", &[0; 4])]), "no fmt"),
2040 (
2041 "law.wav",
2042 &wav(&[chunk(b"fmt ", &fmt(7, 1, 8000, 8)), chunk(b"data", &[])]),
2043 "mu-law",
2044 ),
2045 (
2046 "mute.wav",
2047 &wav(&[chunk(b"fmt ", &fmt(1, 0, 8000, 16)), chunk(b"data", &[])]),
2048 "0 channels",
2049 ),
2050 ("nocomm.aiff", &no_comm, "no COMM"),
2051 (
2052 "packed.aifc",
2053 &aiff(b"AIFC", &comm(1, 1, 16, Some(b"ACE2")), &[0; 2], &[]),
2054 "\"ACE2\"",
2055 ),
2056 ],
2057 );
2058 }
2059
2060 #[test]
2061 fn hostile_headers_are_errors() {
2062 let refused = |bytes: &[u8], says: &str| {
2063 let err = read_header(bytes).unwrap_err().to_string();
2064 assert!(err.contains(says), "{says:?} in {err:?}");
2065 };
2066 refused(b"RIFF", "too short");
2067 refused(&wav(&[chunk(b"data", &[0; 4])]), "no fmt");
2068 refused(&wav(&[chunk(b"fmt ", &fmt(1, 1, 8000, 16))]), "no data");
2069 refused(
2070 &wav(&[chunk(b"fmt ", &fmt(1, 0, 8000, 16)), chunk(b"data", &[])]),
2071 "0 channels",
2072 );
2073 refused(
2074 &wav(&[chunk(b"fmt ", &fmt(1, 2000, 8000, 16)), chunk(b"data", &[])]),
2075 "up to 1024",
2076 );
2077 refused(
2078 &wav(&[chunk(b"fmt ", &fmt(1, 1, 0, 16)), chunk(b"data", &[])]),
2079 "sample rate",
2080 );
2081 refused(
2082 &wav(&[chunk(b"fmt ", &fmt(7, 1, 8000, 8)), chunk(b"data", &[])]),
2083 "mu-law",
2084 );
2085 refused(
2086 &wav(&[chunk(b"fmt ", &fmt(1, 1, 8000, 40)), chunk(b"data", &[])]),
2087 "40-bit",
2088 );
2089 let mut small = fmt(1, 2, 8000, 16);
2091 small[12..14].copy_from_slice(&2u16.to_le_bytes());
2092 refused(
2093 &wav(&[chunk(b"fmt ", &small), chunk(b"data", &[])]),
2094 "cannot hold",
2095 );
2096 let mut cue = u32::MAX.to_le_bytes().to_vec();
2098 cue.extend_from_slice(&[0; 24]);
2099 let bytes = wav(&[
2100 chunk(b"cue ", &cue),
2101 chunk(b"fmt ", &fmt(1, 1, 8000, 16)),
2102 chunk(b"data", &[0; 2]),
2103 ]);
2104 assert_eq!(read_header(&bytes).unwrap().markers.len(), 1);
2105
2106 let mut bytes = wav(&[chunk(b"fmt ", &fmt(1, 2, 8000, 16))]);
2110 bytes.extend_from_slice(b"data");
2111 bytes.extend_from_slice(&0xFFFF_FFF0u32.to_le_bytes());
2112 bytes.extend_from_slice(&i16s(&[1, 2, 3, 4]));
2113 let (_f, source) = open(&bytes, false);
2114 assert_eq!(source.frames(), 2);
2115 let df = source.window(0, u64::MAX, None).unwrap();
2116 assert_eq!(df.height(), 2);
2117 assert_eq!(source.window(u64::MAX, u64::MAX, None).unwrap().height(), 0);
2118 let mut bytes = wav(&[chunk(b"fmt ", &fmt(1, 1, 8000, 16))]);
2120 bytes.extend_from_slice(b"JUNK");
2121 bytes.extend_from_slice(&u32::MAX.to_le_bytes());
2122 refused(&bytes, "no data");
2123 }
2124
2125 #[test]
2126 fn audio_is_known_by_its_first_bytes() {
2127 assert!(looks_like_audio(b"RIFF\0\0\0\0WAVEfmt "));
2128 assert!(looks_like_audio(b"RF64\xff\xff\xff\xffWAVE"));
2129 assert!(looks_like_audio(b"FORM\0\0\0\0AIFC"));
2130 assert!(!looks_like_audio(b"RIFF\0\0\0\0AVI "));
2131 assert!(!looks_like_audio(b"FORM"));
2132 }
2133
2134 #[test]
2137 fn a_long_recording_counts_and_slices_without_reading_what_is_before() {
2138 let dir = tempfile::tempdir().unwrap();
2139 let path = dir.path().join("long.wav");
2140 let mut bytes = wav(&[chunk(b"fmt ", &fmt(1, 1, 48000, 16))]);
2141 bytes.extend_from_slice(b"data");
2142 bytes.extend_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
2143 let header = bytes.len() as u64;
2144 std::fs::write(&path, &bytes).unwrap();
2145 let frames = 20_000_000u64;
2147 let file = std::fs::OpenOptions::new().write(true).open(&path).unwrap();
2148 file.set_len(header + frames * 2).unwrap();
2149 use std::io::{Seek, SeekFrom};
2150 let mut file = file;
2151 file.seek(SeekFrom::Start(header + (frames - 3) * 2))
2152 .unwrap();
2153 file.write_all(&1234i16.to_le_bytes()).unwrap();
2154 drop(file);
2155
2156 let source = Arc::new(AudioSource::open(&path, false).unwrap());
2157 assert_eq!(source.frames(), frames);
2158 let lf = source.lazy();
2159 let streaming = |lf: LazyFrame| crate::statistics::collect_lazy(lf, true).unwrap();
2161 let n = streaming(lf.clone().select([len()]));
2162 assert_eq!(
2163 n.column("len").unwrap().u32().unwrap().get(0),
2164 Some(frames as u32)
2165 );
2166 let tail = streaming(lf.clone().slice((frames - 4) as i64, 10));
2167 assert_eq!(
2168 ints(&tail, "frame"),
2169 [19_999_996, 19_999_997, 19_999_998, 19_999_999]
2170 );
2171 assert_eq!(ints(&tail, "ch1"), [0, 1234, 0, 0]);
2172 let window = source.window(frames - 3, 1, None).unwrap();
2173 assert_eq!(ints(&window, "ch1"), [1234]);
2174 }
2175
2176 #[test]
2177 fn the_signal_report_counts_runs_at_full_scale_and_of_zeros_and_the_mean() {
2178 let mut values = vec![1000i16; 100];
2181 values.extend([i16::MAX; 4]);
2182 values.push(500);
2183 values.push(i16::MIN);
2184 values.extend([0; 20]);
2185 values.extend([1000; 10]);
2186 let bytes = wav(&[
2187 chunk(b"fmt ", &fmt(1, 1, 1000, 16)),
2188 chunk(b"data", &i16s(&values)),
2189 ]);
2190 let (_f, source) = open(&bytes, false);
2191 let report = &source.signal_report(&|| false).unwrap().unwrap()[0];
2192 assert_eq!(report.full_scale, (-32768.0, 32767.0));
2193 assert_eq!(report.at_full_scale, 5);
2194 assert_eq!(
2195 (report.clip_runs, report.in_clip_runs, report.longest_clip),
2196 (1, 4, 4)
2197 );
2198 assert_eq!(report.zero_run_min, 16);
2199 assert_eq!(
2200 (report.zero_runs, report.in_zero_runs, report.longest_zeros),
2201 (1, 20, 20)
2202 );
2203 let sum: f64 = values.iter().map(|&v| v as f64).sum();
2204 assert!((report.mean - sum / values.len() as f64).abs() < 1e-9);
2205
2206 let (_f, source) = open(&bytes, true);
2208 let report = &source.signal_report(&|| false).unwrap().unwrap()[0];
2209 assert_eq!(report.full_scale.0, -1.0);
2210 assert_eq!(report.clip_runs, 1);
2211 assert!(report.mean < 1.0);
2212
2213 assert!(source.signal_report(&|| true).unwrap().is_none());
2215
2216 let peak: Vec<u8> = [i32::MAX; 3].iter().flat_map(|v| v.to_le_bytes()).collect();
2219 let bytes = wav(&[chunk(b"fmt ", &fmt(1, 1, 1000, 32)), chunk(b"data", &peak)]);
2220 let (_f, source) = open(&bytes, true);
2221 let report = &source.signal_report(&|| false).unwrap().unwrap()[0];
2222 assert_eq!(report.clip_runs, 1);
2223 let column = source.window(0, 3, None).unwrap();
2224 let high = report.full_scale.1 as f32;
2225 assert!(
2226 column
2227 .column("ch1")
2228 .unwrap()
2229 .f32()
2230 .unwrap()
2231 .into_no_null_iter()
2232 .all(|v| v >= high)
2233 );
2234 }
2235}