1use std::fs::File;
10use std::path::Path;
11use std::sync::Arc;
12
13use color_eyre::Result;
14use color_eyre::eyre::eyre;
15use memmap2::Mmap;
16use polars::prelude::*;
17
18pub(crate) const READER: crate::formats::readers::Reader = crate::formats::readers::Reader {
20 scan,
21 signatures: &[crate::formats::readers::Signature {
22 says: |head, _| looks_like_audio(head),
23 kind: crate::formats::readers::Kind::Magic,
24 trusted: crate::formats::readers::EVERYWHERE,
25 }],
26 ..crate::formats::readers::BASE
27};
28
29const MAX_CHANNELS: u16 = 1024;
31const MAX_CHUNKS: usize = 1 << 16;
34const MAX_MARKERS: usize = 100_000;
36const MAX_TEXT: usize = 1 << 20;
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum Container {
42 Wav,
44 Rf64,
46 Aiff,
47 Aifc,
49}
50
51impl Container {
52 pub fn label(self) -> &'static str {
53 match self {
54 Container::Wav => "WAV",
55 Container::Rf64 => "RF64",
56 Container::Aiff => "AIFF",
57 Container::Aifc => "AIFF-C",
58 }
59 }
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum Sample {
65 U8,
67 I8,
68 I16,
69 I24,
70 I32,
71 F32,
72 F64,
73}
74
75impl Sample {
76 pub fn bytes(self) -> usize {
78 match self {
79 Sample::U8 | Sample::I8 => 1,
80 Sample::I16 => 2,
81 Sample::I24 => 3,
82 Sample::I32 | Sample::F32 => 4,
83 Sample::F64 => 8,
84 }
85 }
86
87 pub fn is_float(self) -> bool {
88 matches!(self, Sample::F32 | Sample::F64)
89 }
90
91 pub fn dtype(self, normalize: bool) -> DataType {
94 match self {
95 Sample::F32 => DataType::Float32,
96 Sample::F64 => DataType::Float64,
97 _ if normalize => DataType::Float32,
98 Sample::U8 | Sample::I8 => DataType::Int8,
99 Sample::I16 => DataType::Int16,
100 Sample::I24 | Sample::I32 => DataType::Int32,
101 }
102 }
103
104 pub fn full_scale(self) -> f64 {
107 match self {
108 Sample::F32 | Sample::F64 => 1.0,
109 other => (1u64 << (other.bytes() * 8 - 1)) as f64,
110 }
111 }
112
113 pub fn label(self) -> String {
115 let kind = if self.is_float() { "float" } else { "integer" };
116 format!("{}-bit {kind}", self.bytes() * 8)
117 }
118}
119
120#[derive(Debug, Clone, PartialEq)]
122pub struct Marker {
123 pub id: u32,
124 pub sample: u64,
126 pub label: String,
127 pub length: Option<u64>,
129}
130
131#[derive(Debug, Clone, PartialEq)]
134pub struct AudioHeader {
135 pub container: Container,
136 pub broadcast: bool,
138 pub encoding: String,
140 pub sample: Sample,
141 pub big_endian: bool,
142 pub channels: u16,
143 pub sample_rate: f64,
144 pub valid_bits: u16,
146 pub frame_bytes: usize,
148 pub channel_names: Vec<String>,
150 pub data_offset: u64,
152 pub data_declared: Option<u64>,
155 pub metadata: Vec<(String, String)>,
157 pub markers: Vec<Marker>,
158}
159
160impl AudioHeader {
161 pub fn data_len(&self, file_len: u64) -> u64 {
164 let available = file_len.saturating_sub(self.data_offset);
165 self.data_declared
166 .map_or(available, |declared| declared.min(available))
167 }
168
169 pub fn frames(&self, file_len: u64) -> u64 {
171 self.data_len(file_len) / self.frame_bytes as u64
172 }
173
174 pub fn seconds(&self, frame: u64) -> f64 {
176 frame as f64 / self.sample_rate
177 }
178}
179
180pub fn looks_like_audio(head: &[u8]) -> bool {
182 head.len() >= 12
183 && ((matches!(&head[0..4], b"RIFF" | b"RF64" | b"BW64") && &head[8..12] == b"WAVE")
184 || (&head[0..4] == b"FORM" && matches!(&head[8..12], b"AIFF" | b"AIFC")))
185}
186
187pub fn read_header(bytes: &[u8]) -> Result<AudioHeader> {
189 if bytes.len() < 12 {
190 return Err(eyre!("Not an audio file: too short for a header"));
191 }
192 match (&bytes[0..4], &bytes[8..12]) {
193 (b"RIFF" | b"RF64" | b"BW64", b"WAVE") => read_wave(bytes),
194 (b"FORM", b"AIFF" | b"AIFC") => read_aiff(bytes),
195 (b"RIFX", _) => Err(eyre!("Big-endian WAV (RIFX) is not supported")),
196 _ => Err(eyre!("Not a WAV or AIFF file")),
197 }
198}
199
200fn le16(b: &[u8], at: usize) -> u16 {
201 u16::from_le_bytes([b[at], b[at + 1]])
202}
203fn le32(b: &[u8], at: usize) -> u32 {
204 u32::from_le_bytes([b[at], b[at + 1], b[at + 2], b[at + 3]])
205}
206fn le64(b: &[u8], at: usize) -> u64 {
207 let mut a = [0u8; 8];
208 a.copy_from_slice(&b[at..at + 8]);
209 u64::from_le_bytes(a)
210}
211fn be16(b: &[u8], at: usize) -> u16 {
212 u16::from_be_bytes([b[at], b[at + 1]])
213}
214fn be32(b: &[u8], at: usize) -> u32 {
215 u32::from_be_bytes([b[at], b[at + 1], b[at + 2], b[at + 3]])
216}
217
218fn text(raw: &[u8]) -> String {
221 let raw = &raw[..raw.len().min(MAX_TEXT)];
222 let end = raw.iter().position(|&b| b == 0).unwrap_or(raw.len());
223 String::from_utf8_lossy(&raw[..end])
224 .chars()
225 .filter(|c| !c.is_control() || matches!(c, '\n' | '\t'))
226 .collect::<String>()
227 .replace("\r\n", "\n")
228 .trim()
229 .to_string()
230}
231
232const SPEAKERS: [&str; 18] = [
234 "L", "R", "C", "LFE", "BL", "BR", "FLC", "FRC", "BC", "SL", "SR", "TC", "TFL", "TFC", "TFR",
235 "TBL", "TBC", "TBR",
236];
237
238fn channel_names(channels: u16, mask: u32) -> Vec<String> {
241 let mut named = SPEAKERS
242 .iter()
243 .enumerate()
244 .filter(|(bit, _)| mask & (1 << bit) != 0)
245 .map(|(_, name)| name.to_string());
246 (1..=channels)
247 .map(|n| named.next().unwrap_or_else(|| format!("ch{n}")))
248 .collect()
249}
250
251const SUBTYPE_TAIL: [u8; 14] = [
253 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71,
254];
255
256fn tag_name(tag: u16) -> String {
258 match tag {
259 0x0002 => "ADPCM".into(),
260 0x0006 => "A-law".into(),
261 0x0007 => "mu-law".into(),
262 0x0011 => "IMA ADPCM".into(),
263 0x0050 | 0x0055 => "MPEG".into(),
264 other => format!("format tag 0x{other:04X}"),
265 }
266}
267
268struct Fmt {
269 sample: Sample,
270 encoding: String,
271 channels: u16,
272 rate: u32,
273 block_align: u16,
274 valid_bits: u16,
275 mask: u32,
276}
277
278fn read_fmt(body: &[u8]) -> Result<Fmt> {
279 if body.len() < 16 {
280 return Err(eyre!(
281 "WAV fmt chunk is {} bytes; at least 16 needed",
282 body.len()
283 ));
284 }
285 let mut tag = le16(body, 0);
286 let channels = le16(body, 2);
287 let rate = le32(body, 4);
288 let block_align = le16(body, 12);
289 let bits = le16(body, 14);
290 let mut valid_bits = bits;
291 let mut mask = 0;
292 let mut encoding = String::new();
293 if tag == 0xFFFE {
294 if body.len() < 40 {
295 return Err(eyre!(
296 "WAV extensible fmt chunk is {} bytes; 40 needed",
297 body.len()
298 ));
299 }
300 valid_bits = le16(body, 18);
301 mask = le32(body, 20);
302 if body[26..40] != SUBTYPE_TAIL {
303 return Err(eyre!("WAV extensible subformat is not PCM or float"));
304 }
305 tag = le16(body, 24);
306 encoding.push_str("extensible ");
307 }
308 let container = match block_align.checked_rem(channels) {
312 Some(0) if tag == 1 && (9..=32).contains(&bits) => {
313 (block_align / channels).clamp(bits.div_ceil(8), 4)
314 }
315 _ => bits.div_ceil(8),
316 };
317 let sample = match (tag, container) {
318 (1, 1) => Sample::U8,
319 (1, 2) => Sample::I16,
320 (1, 3) => Sample::I24,
321 (1, 4) => Sample::I32,
322 (3, 4) if bits == 32 => Sample::F32,
323 (3, 8) if bits == 64 => Sample::F64,
324 (1 | 3, _) => {
325 return Err(eyre!("WAV with {bits}-bit samples is not supported"));
326 }
327 (other, _) => {
328 return Err(eyre!(
329 "WAV {} audio is not supported; datui reads PCM and float",
330 tag_name(other)
331 ));
332 }
333 };
334 encoding.push_str(if tag == 3 { "IEEE float" } else { "PCM" });
335 if valid_bits == 0 || valid_bits > bits {
336 valid_bits = bits;
337 }
338 Ok(Fmt {
339 sample,
340 encoding,
341 channels,
342 rate,
343 block_align,
344 valid_bits,
345 mask,
346 })
347}
348
349fn frame_bytes(channels: u16, sample: Sample, block_align: Option<u16>) -> Result<usize> {
351 if channels == 0 {
352 return Err(eyre!("Audio header says 0 channels"));
353 }
354 if channels > MAX_CHANNELS {
355 return Err(eyre!(
356 "Audio header says {channels} channels; datui reads up to {MAX_CHANNELS}"
357 ));
358 }
359 let packed = channels as usize * sample.bytes();
360 match block_align {
361 Some(align) if (align as usize) < packed => Err(eyre!(
363 "Audio header's frame size ({align} bytes) cannot hold {channels} channels of {}",
364 sample.label()
365 )),
366 Some(align) => Ok(align as usize),
367 None => Ok(packed),
368 }
369}
370
371fn check_rate(rate: f64) -> Result<f64> {
372 if rate.is_finite() && (1.0..=1e9).contains(&rate) {
373 Ok(rate)
374 } else {
375 Err(eyre!("Audio header's sample rate ({rate}) is not usable"))
376 }
377}
378
379fn chunks(bytes: &[u8], big_endian: bool) -> impl Iterator<Item = ([u8; 4], u64, u64)> + '_ {
383 let len = bytes.len() as u64;
384 let mut pos: u64 = 12;
385 let mut seen = 0;
386 std::iter::from_fn(move || {
387 if seen >= MAX_CHUNKS || pos.checked_add(8)? > len {
388 return None;
389 }
390 seen += 1;
391 let at = pos as usize;
392 let mut id = [0u8; 4];
393 id.copy_from_slice(&bytes[at..at + 4]);
394 let size = if big_endian {
395 be32(bytes, at + 4)
396 } else {
397 le32(bytes, at + 4)
398 } as u64;
399 let body = pos + 8;
400 pos = body.saturating_add(size).saturating_add(size & 1);
403 Some((id, body, size))
404 })
405}
406
407fn body_of(bytes: &[u8], start: u64, size: u64) -> Option<&[u8]> {
409 let end = start.checked_add(size)?;
410 bytes.get(start as usize..usize::try_from(end).ok()?)
411}
412
413fn read_wave(bytes: &[u8]) -> Result<AudioHeader> {
414 let len = bytes.len() as u64;
415 let container = match &bytes[0..4] {
416 b"RIFF" => Container::Wav,
417 _ => Container::Rf64,
418 };
419 let mut fmt = None;
420 let mut ds64_data = None;
421 let mut data: Option<(u64, Option<u64>)> = None;
422 let mut metadata = Vec::new();
423 let mut broadcast = false;
424 let mut cues: Vec<(u32, u64)> = Vec::new();
425 let mut labels: Vec<(u32, String)> = Vec::new();
426 let mut lengths: Vec<(u32, u64)> = Vec::new();
427 for (id, start, size) in chunks(bytes, false) {
428 if &id == b"data" {
429 let stated = match (container, size) {
430 (Container::Rf64, 0xFFFF_FFFF) => ds64_data,
431 (_, 0 | 0xFFFF_FFFF) => None,
432 (Container::Wav, size) => Some(unwrapped_size(size, len - start)),
433 (_, size) => Some(size),
434 };
435 let runs_on = stated.is_none_or(|s| start.saturating_add(s) >= len);
438 data = Some((start, stated.filter(|&s| s > 0)));
439 if runs_on {
440 break;
441 }
442 continue;
443 }
444 let Some(body) = body_of(bytes, start, size) else {
445 break;
448 };
449 match &id {
450 b"fmt " => fmt = Some(read_fmt(body)?),
451 b"ds64" if body.len() >= 24 => ds64_data = Some(le64(body, 8)),
452 b"bext" => {
453 broadcast = true;
454 read_bext(body, &mut metadata);
455 }
456 b"iXML" => read_ixml(body, &mut metadata),
457 b"cue " => read_cue(body, &mut cues),
458 b"LIST" if body.len() >= 4 => match &body[0..4] {
459 b"adtl" => read_adtl(&body[4..], &mut labels, &mut lengths),
460 b"INFO" => read_info(&body[4..], &mut metadata),
461 _ => {}
462 },
463 _ => {}
464 }
465 }
466 let fmt = fmt.ok_or_else(|| eyre!("WAV file has no fmt chunk"))?;
467 if let Some((_, value)) = metadata
470 .iter_mut()
471 .find(|(key, _)| key == "bext.time_reference")
472 && let Some(samples) = value
473 .strip_suffix(" samples")
474 .and_then(|n| n.parse::<u64>().ok())
475 && fmt.rate > 0
476 {
477 let ms = (samples as u128 * 1000 / fmt.rate as u128) as u64;
478 *value = format!(
479 "{:02}:{:02}:{:02}.{:03} ({samples} samples)",
480 ms / 3_600_000,
481 ms / 60_000 % 60,
482 ms / 1000 % 60,
483 ms % 1000
484 );
485 }
486 let (data_offset, data_declared) = data.ok_or_else(|| eyre!("WAV file has no data chunk"))?;
487 let frame_bytes = frame_bytes(fmt.channels, fmt.sample, Some(fmt.block_align))?;
488 let sample_rate = check_rate(fmt.rate as f64)?;
489 let markers = cues
490 .into_iter()
491 .map(|(id, sample)| Marker {
492 id,
493 sample,
494 label: labels
495 .iter()
496 .find(|(l, _)| *l == id)
497 .map(|(_, s)| s.clone())
498 .unwrap_or_default(),
499 length: lengths.iter().find(|(l, _)| *l == id).map(|(_, n)| *n),
500 })
501 .collect();
502 Ok(AudioHeader {
503 container,
504 broadcast,
505 encoding: fmt.encoding,
506 sample: fmt.sample,
507 big_endian: false,
508 channels: fmt.channels,
509 sample_rate,
510 valid_bits: fmt.valid_bits,
511 frame_bytes,
512 channel_names: channel_names(fmt.channels, fmt.mask),
513 data_offset,
514 data_declared,
515 metadata,
516 markers,
517 })
518}
519
520fn unwrapped_size(stated: u64, available: u64) -> u64 {
524 if available <= u32::MAX as u64 || stated > available {
525 return stated;
526 }
527 stated + ((available - stated) >> 32 << 32)
528}
529
530fn read_bext(body: &[u8], metadata: &mut Vec<(String, String)>) {
532 let field = |from: usize, to: usize| body.get(from..to).map(text).unwrap_or_default();
533 let mut put = |key: &str, value: String| {
534 if !value.is_empty() {
535 metadata.push((key.to_string(), value));
536 }
537 };
538 put("bext.description", field(0, 256));
539 put("bext.originator", field(256, 288));
540 put("bext.originator_reference", field(288, 320));
541 let date = field(320, 330);
542 let time = field(330, 338);
543 put(
544 "bext.origination",
545 format!("{date} {time}").trim().to_string(),
546 );
547 if body.len() >= 346 {
548 let samples = le32(body, 338) as u64 | ((le32(body, 342) as u64) << 32);
549 put("bext.time_reference", format!("{samples} samples"));
550 }
551 if body.len() >= 348 {
552 put("bext.version", le16(body, 346).to_string());
553 }
554 if body.len() > 602 {
555 put("bext.coding_history", field(602, body.len()));
556 }
557}
558
559fn read_ixml(body: &[u8], metadata: &mut Vec<(String, String)>) {
562 let doc = text(body);
563 for tag in ["PROJECT", "SCENE", "TAKE", "TAPE", "NOTE"] {
564 let open = format!("<{tag}>");
565 let close = format!("</{tag}>");
566 if let Some(from) = doc.find(&open).map(|i| i + open.len())
567 && let Some(len) = doc[from..].find(&close)
568 {
569 let value = doc[from..from + len].trim();
570 if !value.is_empty() {
571 metadata.push((format!("ixml.{}", tag.to_lowercase()), value.to_string()));
572 }
573 }
574 }
575 if !doc.is_empty() {
576 metadata.push(("ixml".to_string(), doc));
577 }
578}
579
580fn read_cue(body: &[u8], cues: &mut Vec<(u32, u64)>) {
581 if body.len() < 4 {
582 return;
583 }
584 let fits = (body.len() - 4) / 24;
586 let count = (le32(body, 0) as usize).min(fits).min(MAX_MARKERS);
587 for i in 0..count {
588 let at = 4 + i * 24;
589 cues.push((le32(body, at), le32(body, at + 20) as u64));
590 }
591}
592
593fn sub_chunks(body: &[u8]) -> impl Iterator<Item = ([u8; 4], &[u8])> + '_ {
595 let mut pos = 0usize;
596 std::iter::from_fn(move || {
597 let head = body.get(pos..pos.checked_add(8)?)?;
598 let mut id = [0u8; 4];
599 id.copy_from_slice(&head[0..4]);
600 let size = le32(head, 4) as usize;
601 let start = pos + 8;
602 let sub = body.get(start..start.checked_add(size)?)?;
603 pos = start + size + (size & 1);
604 Some((id, sub))
605 })
606}
607
608fn read_adtl(body: &[u8], labels: &mut Vec<(u32, String)>, lengths: &mut Vec<(u32, u64)>) {
609 for (id, sub) in sub_chunks(body).take(MAX_MARKERS) {
610 if sub.len() < 4 {
611 continue;
612 }
613 let cue = le32(sub, 0);
614 match &id {
615 b"labl" => labels.push((cue, text(&sub[4..]))),
616 b"ltxt" if sub.len() >= 8 => lengths.push((cue, le32(sub, 4) as u64)),
617 _ => {}
618 }
619 }
620}
621
622fn read_info(body: &[u8], metadata: &mut Vec<(String, String)>) {
623 for (id, sub) in sub_chunks(body).take(256) {
624 let key = match &id {
625 b"INAM" => "title",
626 b"IART" => "artist",
627 b"ICMT" => "comment",
628 b"ICRD" => "date",
629 b"ISFT" => "software",
630 b"IENG" => "engineer",
631 b"ICOP" => "copyright",
632 b"IPRD" => "product",
633 b"IGNR" => "genre",
634 _ => continue,
635 };
636 let value = text(sub);
637 if !value.is_empty() {
638 metadata.push((format!("info.{key}"), value));
639 }
640 }
641}
642
643fn extended_to_f64(b: &[u8]) -> f64 {
645 let sign_exp = u16::from_be_bytes([b[0], b[1]]);
646 let mut m = [0u8; 8];
647 m.copy_from_slice(&b[2..10]);
648 let mantissa = u64::from_be_bytes(m);
649 if mantissa == 0 {
650 return 0.0;
651 }
652 let exponent = (sign_exp & 0x7FFF) as i32 - 16383 - 63;
653 let value = mantissa as f64 * 2f64.powi(exponent);
654 if sign_exp & 0x8000 != 0 {
655 -value
656 } else {
657 value
658 }
659}
660
661fn pstring(b: &[u8]) -> Option<(String, usize)> {
664 let n = *b.first()? as usize;
665 let raw = b.get(1..1 + n)?;
666 let taken = 1 + n + ((1 + n) & 1);
667 Some((text(raw), taken))
668}
669
670fn read_aiff(bytes: &[u8]) -> Result<AudioHeader> {
671 let len = bytes.len() as u64;
672 let aifc = &bytes[8..12] == b"AIFC";
673 let mut comm = None;
674 let mut data: Option<(u64, Option<u64>)> = None;
675 let mut metadata = Vec::new();
676 let mut markers = Vec::new();
677 for (id, start, size) in chunks(bytes, true) {
678 if &id == b"SSND" {
679 let Some(head) = body_of(bytes, start, 8) else {
680 break;
681 };
682 let offset = be32(head, 0) as u64;
683 let data_start = start + 8 + offset;
684 let stated = (size > 0)
686 .then(|| size.checked_sub(8 + offset))
687 .flatten()
688 .filter(|&s| s > 0);
689 let runs_on = stated.is_none_or(|s| data_start.saturating_add(s) >= len);
690 data = Some((data_start, stated));
691 if runs_on {
692 break;
693 }
694 continue;
695 }
696 let Some(body) = body_of(bytes, start, size) else {
697 break;
698 };
699 match &id {
700 b"COMM" => comm = Some(read_comm(body, aifc)?),
701 b"MARK" if body.len() >= 2 => {
702 let count = (be16(body, 0) as usize).min(MAX_MARKERS);
703 let mut at = 2;
704 for _ in 0..count {
705 let Some(head) = body.get(at..at + 6) else {
706 break;
707 };
708 let id = be16(head, 0) as u32;
709 let sample = be32(head, 2) as u64;
710 let Some((label, taken)) = body.get(at + 6..).and_then(pstring) else {
711 break;
712 };
713 markers.push(Marker {
714 id,
715 sample,
716 label,
717 length: None,
718 });
719 at += 6 + taken;
720 }
721 }
722 b"NAME" | b"AUTH" | b"(c) " | b"ANNO" => {
723 let key = match &id {
724 b"NAME" => "name",
725 b"AUTH" => "author",
726 b"(c) " => "copyright",
727 _ => "annotation",
728 };
729 let value = text(body);
730 if !value.is_empty() {
731 metadata.push((key.to_string(), value));
732 }
733 }
734 _ => {}
735 }
736 }
737 let comm = comm.ok_or_else(|| eyre!("AIFF file has no COMM chunk"))?;
738 let (data_offset, mut data_declared) =
739 data.ok_or_else(|| eyre!("AIFF file has no SSND chunk"))?;
740 let frame_bytes = frame_bytes(comm.channels, comm.sample, None)?;
741 if comm.frames > 0 {
743 let by_comm = comm.frames.saturating_mul(frame_bytes as u64);
744 data_declared = Some(data_declared.map_or(by_comm, |d| d.min(by_comm)));
745 }
746 Ok(AudioHeader {
747 container: if aifc {
748 Container::Aifc
749 } else {
750 Container::Aiff
751 },
752 broadcast: false,
753 encoding: comm.encoding,
754 sample: comm.sample,
755 big_endian: comm.big_endian,
756 channels: comm.channels,
757 sample_rate: comm.rate,
758 valid_bits: comm.valid_bits,
759 frame_bytes,
760 channel_names: channel_names(comm.channels, 0),
761 data_offset,
762 data_declared,
763 metadata,
764 markers,
765 })
766}
767
768struct Comm {
769 channels: u16,
770 frames: u64,
771 sample: Sample,
772 big_endian: bool,
773 rate: f64,
774 valid_bits: u16,
775 encoding: String,
776}
777
778fn read_comm(body: &[u8], aifc: bool) -> Result<Comm> {
779 if body.len() < 18 {
780 return Err(eyre!("AIFF COMM chunk is {} bytes; 18 needed", body.len()));
781 }
782 let channels = be16(body, 0);
783 let frames = be32(body, 2) as u64;
784 let bits = be16(body, 6);
785 let rate = check_rate(extended_to_f64(&body[8..18]))?;
786 let code: [u8; 4] = match body.get(18..22) {
787 Some(c) if aifc => [c[0], c[1], c[2], c[3]],
788 _ => *b"NONE",
789 };
790 let int = |bits: u16| match bits.div_ceil(8) {
791 1 => Some(Sample::I8),
792 2 => Some(Sample::I16),
793 3 => Some(Sample::I24),
794 4 => Some(Sample::I32),
795 _ => None,
796 };
797 let (sample, big_endian) = match &code {
798 b"NONE" | b"twos" => (int(bits), true),
799 b"sowt" => (int(bits), false),
800 b"raw " if bits <= 8 => (Some(Sample::U8), true),
801 b"in24" => (Some(Sample::I24), true),
802 b"in32" => (Some(Sample::I32), true),
803 b"23ni" => (Some(Sample::I24), false),
804 b"fl32" | b"FL32" => (Some(Sample::F32), true),
805 b"fl64" | b"FL64" => (Some(Sample::F64), true),
806 other => {
807 return Err(eyre!(
808 "AIFF-C compression \"{}\" is not supported; datui reads uncompressed audio",
809 String::from_utf8_lossy(other)
810 ));
811 }
812 };
813 let sample = sample.ok_or_else(|| eyre!("AIFF with {bits}-bit samples is not supported"))?;
814 let encoding = match &code {
815 b"NONE" | b"twos" => "PCM".to_string(),
816 b"sowt" => "PCM, little-endian".to_string(),
817 b"fl32" | b"FL32" | b"fl64" | b"FL64" => "IEEE float".to_string(),
818 other => format!("PCM ({})", String::from_utf8_lossy(other).trim()),
819 };
820 let container_bits = (sample.bytes() * 8) as u16;
821 let valid_bits = if sample.is_float() || bits == 0 || bits > container_bits {
822 container_bits
823 } else {
824 bits
825 };
826 Ok(Comm {
827 channels,
828 frames,
829 sample,
830 big_endian,
831 rate,
832 valid_bits,
833 encoding,
834 })
835}
836
837pub struct AudioSource {
839 file: Option<File>,
841 map: Mmap,
842 header: AudioHeader,
843 frames: u64,
844 normalize: bool,
846}
847
848impl crate::formats::pushdown::Windowed for AudioSource {
851 fn window(&self, start: usize, len: usize) -> PolarsResult<LazyFrame> {
852 Ok(AudioSource::window(self, start as u64, len as u64, None)?.lazy())
853 }
854}
855
856impl std::fmt::Debug for AudioSource {
857 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
858 f.debug_struct("AudioSource")
859 .field("header", &self.header)
860 .field("frames", &self.frames)
861 .field("normalize", &self.normalize)
862 .finish()
863 }
864}
865
866pub const FRAME: &str = "frame";
868pub const SECONDS: &str = "seconds";
872const MAX_FRAMES: u64 = crate::formats::row_index::MAX_ROWS as u64;
874
875impl AudioSource {
876 pub fn open(path: &Path, normalize: bool) -> Result<Self> {
877 let file = File::open(path)?;
878 let map = unsafe { Mmap::map(&file)? };
882 let header = read_header(&map)?;
883 let frames = header.frames(map.len() as u64).min(MAX_FRAMES);
884 Ok(Self {
885 file: Some(file),
886 map,
887 header,
888 frames,
889 normalize,
890 })
891 }
892
893 pub fn from_bytes(bytes: &[u8], normalize: bool) -> Result<Self> {
896 let header = read_header(bytes)?;
897 let mut copy = memmap2::MmapMut::map_anon(bytes.len())?;
898 copy.copy_from_slice(bytes);
899 let map = copy.make_read_only()?;
900 let frames = header.frames(map.len() as u64).min(MAX_FRAMES);
901 Ok(Self {
902 file: None,
903 map,
904 header,
905 frames,
906 normalize,
907 })
908 }
909
910 pub fn header(&self) -> &AudioHeader {
911 &self.header
912 }
913
914 pub fn frames(&self) -> u64 {
915 self.frames
916 }
917
918 pub fn normalize(&self) -> bool {
919 self.normalize
920 }
921
922 fn still_whole(&self) -> PolarsResult<()> {
925 if let Some(file) = &self.file {
926 let len = file.metadata()?.len();
927 polars_ensure!(
928 len >= self.map.len() as u64,
929 ComputeError: "the file is now {len} bytes, shorter than the {} it had when it was opened; open it again",
930 self.map.len()
931 );
932 }
933 Ok(())
934 }
935
936 pub fn seconds(&self) -> f64 {
938 self.header.seconds(self.frames)
939 }
940
941 pub fn frames_past_limit(&self) -> u64 {
943 self.header
944 .frames(self.map.len() as u64)
945 .saturating_sub(self.frames)
946 }
947
948 pub fn trailing_bytes(&self) -> u64 {
950 self.header.data_len(self.map.len() as u64) % self.header.frame_bytes as u64
951 }
952
953 pub fn cut_short(&self) -> Option<(u64, u64)> {
955 let declared = self.header.data_declared?;
956 let held = self.header.data_len(self.map.len() as u64);
957 (declared > held).then_some((declared, held))
958 }
959
960 pub fn schema(&self) -> Schema {
961 let mut schema = Schema::with_capacity(self.header.channels as usize + 2);
962 schema.insert(FRAME.into(), DataType::Int64);
963 schema.insert(SECONDS.into(), DataType::Float64);
964 let dtype = self.header.sample.dtype(self.normalize);
965 for name in &self.header.channel_names {
966 schema.insert(name.as_str().into(), dtype.clone());
967 }
968 schema
969 }
970
971 pub fn window(
974 &self,
975 start: u64,
976 len: u64,
977 columns: Option<&[PlSmallStr]>,
978 ) -> PolarsResult<DataFrame> {
979 self.still_whole()?;
980 let start = start.min(self.frames);
981 let n = len.min(self.frames - start);
982 let schema = self.schema();
983 let all: Vec<PlSmallStr>;
984 let columns = match columns {
985 Some(c) => c,
986 None => {
987 all = schema.iter_names().cloned().collect();
988 &all
989 }
990 };
991 let mut out = Vec::with_capacity(columns.len());
992 for name in columns {
993 let Some(which) = self.which(name) else {
994 polars_bail!(ColumnNotFound: "{name}");
995 };
996 out.push(self.decode(name.clone(), which, (start..start + n).map(Some)));
997 }
998 DataFrame::new(n as usize, out)
999 }
1000
1001 fn which(&self, name: &str) -> Option<Which> {
1003 match name {
1004 FRAME => Some(Which::Frame),
1005 SECONDS => Some(Which::Seconds),
1006 other => self
1007 .header
1008 .channel_names
1009 .iter()
1010 .position(|c| c == other)
1011 .map(Which::Channel),
1012 }
1013 }
1014
1015 fn sample_at(&self, frame: u64, channel: usize) -> Option<&[u8]> {
1017 if frame >= self.frames {
1018 return None;
1019 }
1020 let h = &self.header;
1021 let width = h.sample.bytes();
1022 let at = frame
1023 .checked_mul(h.frame_bytes as u64)?
1024 .checked_add(h.data_offset)?
1025 .checked_add((channel * width) as u64)?;
1026 let at = usize::try_from(at).ok()?;
1027 self.map.get(at..at.checked_add(width)?)
1028 }
1029
1030 fn decode(
1033 &self,
1034 name: PlSmallStr,
1035 which: Which,
1036 frames: impl Iterator<Item = Option<u64>>,
1037 ) -> Column {
1038 let channel = match which {
1039 Which::Frame => {
1040 return Int64Chunked::from_iter_options(name, frames.map(|f| f.map(|f| f as i64)))
1041 .into_column();
1042 }
1043 Which::Seconds => {
1044 return Float64Chunked::from_iter_options(
1045 name,
1046 frames.map(|f| f.map(|f| self.header.seconds(f))),
1047 )
1048 .into_column();
1049 }
1050 Which::Channel(c) => c,
1051 };
1052 let h = &self.header;
1053 let be = h.big_endian;
1054 let scale = 1.0 / h.sample.full_scale() as f32;
1055 let bytes = frames.map(move |f| f.and_then(|f| self.sample_at(f, channel)));
1056 let sample = h.sample;
1057 macro_rules! int_column {
1058 ($chunked:ty, $t:ty) => {{
1059 if self.normalize {
1060 Float32Chunked::from_iter_options(
1061 name,
1062 bytes.map(|b| b.map(|b| int_sample(sample, be, b) as f32 * scale)),
1063 )
1064 .into_column()
1065 } else {
1066 <$chunked>::from_iter_options(
1067 name,
1068 bytes.map(|b| b.map(|b| int_sample(sample, be, b) as $t)),
1069 )
1070 .into_column()
1071 }
1072 }};
1073 }
1074 match sample {
1075 Sample::U8 | Sample::I8 => int_column!(Int8Chunked, i8),
1076 Sample::I16 => int_column!(Int16Chunked, i16),
1077 Sample::I24 | Sample::I32 => int_column!(Int32Chunked, i32),
1078 Sample::F32 => {
1079 Float32Chunked::from_iter_options(name, bytes.map(|b| b.map(|b| f32_sample(be, b))))
1080 .into_column()
1081 }
1082 Sample::F64 => {
1083 Float64Chunked::from_iter_options(name, bytes.map(|b| b.map(|b| f64_sample(be, b))))
1084 .into_column()
1085 }
1086 }
1087 }
1088
1089 pub fn lazy(self: &Arc<Self>) -> LazyFrame {
1093 crate::formats::row_index::lazy(self)
1094 }
1095
1096 pub fn full_scale_bounds(&self) -> (f64, f64) {
1100 let h = &self.header;
1101 if h.sample.is_float() {
1102 return (-1.0, 1.0);
1103 }
1104 let container = (h.sample.bytes() * 8) as u32;
1105 let valid = (h.valid_bits as u32).clamp(1, container);
1106 let step = (1u64 << (container - valid)) as f64;
1108 let low = -h.sample.full_scale();
1109 let high = h.sample.full_scale() - step;
1110 if self.normalize {
1111 let scale = h.sample.full_scale();
1112 (low / scale, (high / scale) as f32 as f64)
1113 } else {
1114 (low, high)
1115 }
1116 }
1117
1118 pub fn signal_report(
1122 &self,
1123 stop: &dyn Fn() -> bool,
1124 ) -> PolarsResult<Option<Vec<SignalReport>>> {
1125 let h = &self.header;
1126 let channels = h.channels as usize;
1127 let (low, high) = self.full_scale_bounds();
1128 let scale = 1.0 / h.sample.full_scale() as f32;
1129 let zero_run = (h.sample_rate / 100.0).round().max(16.0) as u64;
1130 let mut reports: Vec<SignalReport> = h
1131 .channel_names
1132 .iter()
1133 .map(|name| SignalReport {
1134 channel: name.clone(),
1135 frames: self.frames,
1136 full_scale: (low, high),
1137 clip_run_min: CLIP_RUN,
1138 zero_run_min: zero_run,
1139 ..SignalReport::default()
1140 })
1141 .collect();
1142 let mut clip_len = vec![0u64; channels];
1143 let mut zero_len = vec![0u64; channels];
1144 let mut sums = vec![0f64; channels];
1145 let decode = |b: &[u8]| -> f64 {
1148 let be = h.big_endian;
1149 match h.sample {
1150 Sample::F32 => f32_sample(be, b) as f64,
1151 Sample::F64 => f64_sample(be, b),
1152 int if self.normalize => (int_sample(int, be, b) as f32 * scale) as f64,
1153 int => int_sample(int, be, b) as f64,
1154 }
1155 };
1156 let end_run =
1157 |len: &mut u64, min: u64, runs: &mut u64, within: &mut u64, longest: &mut u64| {
1158 if *len >= min {
1159 *runs += 1;
1160 *within += *len;
1161 *longest = (*longest).max(*len);
1162 }
1163 *len = 0;
1164 };
1165 for frame in 0..self.frames {
1166 if frame % (1 << 20) == 0 {
1167 if stop() {
1168 return Ok(None);
1169 }
1170 self.still_whole()?;
1171 }
1172 for c in 0..channels {
1173 let Some(bytes) = self.sample_at(frame, c) else {
1174 continue;
1175 };
1176 let v = decode(bytes);
1177 let r = &mut reports[c];
1178 if !v.is_finite() {
1179 continue;
1180 }
1181 sums[c] += v;
1182 if v <= low || v >= high {
1183 r.at_full_scale += 1;
1184 clip_len[c] += 1;
1185 } else {
1186 end_run(
1187 &mut clip_len[c],
1188 CLIP_RUN,
1189 &mut r.clip_runs,
1190 &mut r.in_clip_runs,
1191 &mut r.longest_clip,
1192 );
1193 }
1194 if v == 0.0 {
1195 zero_len[c] += 1;
1196 } else {
1197 end_run(
1198 &mut zero_len[c],
1199 zero_run,
1200 &mut r.zero_runs,
1201 &mut r.in_zero_runs,
1202 &mut r.longest_zeros,
1203 );
1204 }
1205 }
1206 }
1207 for (c, r) in reports.iter_mut().enumerate() {
1208 end_run(
1209 &mut clip_len[c],
1210 CLIP_RUN,
1211 &mut r.clip_runs,
1212 &mut r.in_clip_runs,
1213 &mut r.longest_clip,
1214 );
1215 end_run(
1216 &mut zero_len[c],
1217 zero_run,
1218 &mut r.zero_runs,
1219 &mut r.in_zero_runs,
1220 &mut r.longest_zeros,
1221 );
1222 if self.frames > 0 {
1223 r.mean = sums[c] / self.frames as f64;
1224 }
1225 }
1226 Ok(Some(reports))
1227 }
1228}
1229
1230impl crate::formats::row_index::RowSource for AudioSource {
1231 fn height(&self) -> usize {
1232 self.frames as usize
1233 }
1234
1235 fn schema(&self) -> SchemaRef {
1236 Arc::new(AudioSource::schema(self))
1237 }
1238
1239 fn decode(&self, column: usize, index: &IdxCa) -> PolarsResult<Column> {
1240 self.still_whole()?;
1241 let which = match column {
1242 0 => Which::Frame,
1243 1 => Which::Seconds,
1244 c => Which::Channel(c - 2),
1245 };
1246 let name = self.schema().get_at_index(column).map(|(n, _)| n.clone());
1247 let name = name.ok_or_else(|| polars_err!(ColumnNotFound: "column {column}"))?;
1248 Ok(AudioSource::decode(
1249 self,
1250 name,
1251 which,
1252 index.iter().map(|f| f.map(u64::from)),
1253 ))
1254 }
1255}
1256
1257fn int_sample(sample: Sample, be: bool, b: &[u8]) -> i32 {
1260 match sample {
1261 Sample::U8 => (b[0] ^ 0x80) as i8 as i32,
1262 Sample::I8 => b[0] as i8 as i32,
1263 Sample::I16 => {
1264 let a = [b[0], b[1]];
1265 (if be {
1266 i16::from_be_bytes(a)
1267 } else {
1268 i16::from_le_bytes(a)
1269 }) as i32
1270 }
1271 Sample::I24 => {
1272 let a = if be {
1273 [b[2], b[1], b[0], 0]
1274 } else {
1275 [b[0], b[1], b[2], 0]
1276 };
1277 i32::from_le_bytes(a) << 8 >> 8
1279 }
1280 Sample::I32 => {
1281 let a = [b[0], b[1], b[2], b[3]];
1282 if be {
1283 i32::from_be_bytes(a)
1284 } else {
1285 i32::from_le_bytes(a)
1286 }
1287 }
1288 Sample::F32 | Sample::F64 => 0,
1289 }
1290}
1291
1292fn f32_sample(be: bool, b: &[u8]) -> f32 {
1293 let a = [b[0], b[1], b[2], b[3]];
1294 if be {
1295 f32::from_be_bytes(a)
1296 } else {
1297 f32::from_le_bytes(a)
1298 }
1299}
1300
1301fn f64_sample(be: bool, b: &[u8]) -> f64 {
1302 let mut a = [0u8; 8];
1303 a.copy_from_slice(&b[..8]);
1304 if be {
1305 f64::from_be_bytes(a)
1306 } else {
1307 f64::from_le_bytes(a)
1308 }
1309}
1310
1311pub const CLIP_RUN: u64 = 3;
1314
1315#[derive(Debug, Clone, Default, PartialEq)]
1317pub struct SignalReport {
1318 pub channel: String,
1319 pub frames: u64,
1320 pub full_scale: (f64, f64),
1322 pub at_full_scale: u64,
1324 pub clip_run_min: u64,
1327 pub clip_runs: u64,
1328 pub in_clip_runs: u64,
1329 pub longest_clip: u64,
1330 pub zero_run_min: u64,
1333 pub zero_runs: u64,
1334 pub in_zero_runs: u64,
1335 pub longest_zeros: u64,
1336 pub mean: f64,
1338}
1339
1340#[derive(Debug, Clone, Copy)]
1342enum Which {
1343 Frame,
1344 Seconds,
1345 Channel(usize),
1346}
1347
1348pub(crate) fn recording(
1352 window: Arc<dyn crate::formats::pushdown::Windowed>,
1353) -> Option<Arc<AudioSource>> {
1354 let any: Arc<dyn std::any::Any + Send + Sync> = window;
1355 any.downcast::<AudioSource>().ok()
1356}
1357
1358pub fn detail(audio: &AudioSource) -> crate::formats::text_formats::Detail {
1361 use crate::formats::model_files::MetaValue;
1362 use crate::widgets::info::{clock, count_of, group_u64};
1363 let h = audio.header();
1364 let g = crate::glyphs::get();
1365 let sep = format!(" {} ", g.middot);
1366 let mut kind = h.container.label().to_string();
1367 if h.broadcast {
1368 kind.push_str(" (Broadcast WAV)");
1369 }
1370 let rate = if h.sample_rate.fract() == 0.0 {
1371 group_u64(h.sample_rate as u64)
1372 } else {
1373 format!("{:.3}", h.sample_rate)
1374 };
1375 let mut samples = h.sample.label();
1376 if !h.sample.is_float() && (h.valid_bits as usize) < h.sample.bytes() * 8 {
1377 samples.push_str(&format!(" ({} valid)", h.valid_bits));
1378 }
1379 let mut lines = vec![
1380 format!(
1381 "{kind}{sep}{}{sep}{rate} Hz",
1382 count_of(h.channels as u64, "channel", "channels"),
1383 ),
1384 format!(
1385 "Samples: {samples}{sep}{}{}",
1386 h.encoding,
1387 if audio.normalize() && !h.sample.is_float() {
1388 format!("{sep}shown as float in [-1, 1]")
1389 } else {
1390 String::new()
1391 }
1392 ),
1393 format!(
1394 "Frames: {}{sep}Length: {}",
1395 group_u64(audio.frames()),
1396 clock(audio.seconds()),
1397 ),
1398 format!(
1399 "Data: {}",
1400 crate::numfmt::bytes(audio.frames() * h.frame_bytes as u64)
1401 ),
1402 ];
1403 let mut warnings = Vec::new();
1404 if let Some((declared, held)) = audio.cut_short() {
1405 warnings.push(format!(
1406 "header says {} of samples{sep}file holds {}",
1407 crate::numfmt::bytes(declared),
1408 crate::numfmt::bytes(held)
1409 ));
1410 } else if h.data_declared.is_none() && audio.frames() > 0 {
1411 lines.push(format!(
1412 "no data size in header{sep}frames counted from file size"
1413 ));
1414 }
1415 let past = audio.frames_past_limit();
1416 if past > 0 {
1417 warnings.push(format!(
1418 "last {} frames not shown: past the table limit",
1419 group_u64(past)
1420 ));
1421 }
1422 let trailing = audio.trailing_bytes();
1423 if trailing > 0 {
1424 warnings.push(format!(
1425 "{trailing} bytes after the last whole frame not shown"
1426 ));
1427 }
1428 let mut metadata: crate::formats::model_files::Metadata = h
1429 .metadata
1430 .iter()
1431 .map(|(k, v)| (k.clone(), MetaValue::Text(v.clone())))
1432 .collect();
1433 for marker in &h.markers {
1434 let mut value = format!(
1435 "{}{sep}frame {}",
1436 clock(marker.sample as f64 / h.sample_rate),
1437 group_u64(marker.sample)
1438 );
1439 if let Some(length) = marker.length {
1440 value.push_str(&format!(
1441 "{sep}{} long",
1442 clock(length as f64 / h.sample_rate)
1443 ));
1444 }
1445 if !marker.label.is_empty() {
1446 value.push_str(&sep);
1447 value.push_str(&marker.label);
1448 }
1449 metadata.push((format!("marker {}", marker.id), MetaValue::Text(value)));
1450 }
1451 crate::formats::text_formats::Detail {
1452 tab: crate::formats::text_formats::tab(crate::FileFormat::Audio),
1453 lines,
1454 warnings,
1455 list_title: "Metadata",
1456 list: metadata,
1457 first: true,
1460 own_columns: true,
1461 ..Default::default()
1462 }
1463}
1464
1465fn scan(input: crate::formats::readers::ScanIn<'_>) -> Result<crate::loading::scan::Scan> {
1469 let source = Arc::new(AudioSource::open(input.path(), input.options.normalize)?);
1470 let lf = source.lazy();
1471 let rows = usize::try_from(source.frames()).unwrap_or(usize::MAX);
1473 input.report.opened = Some(Arc::new(crate::formats::members::Opened {
1474 detail: Some(Arc::new(detail(&source))),
1475 window: Some((source, rows)),
1476 ..Default::default()
1477 }));
1478 Ok(lf.into())
1479}
1480
1481#[cfg(test)]
1482mod tests;