Skip to main content

datui_lib/formats/
audio.rs

1//! WAV, BWF, RF64 and AIFF audio as a table of sample frames. The file is memory-mapped
2//! and a chunk walker finds the format and samples; row `i` is at
3//! `data_offset + i * frame_bytes`, so any window decodes from its own bytes (a 20 GB
4//! recording reads a screenful). Every stated length is checked against the file
5//! before use, so a hostile header errors rather than panics or over-allocates. The row
6//! count comes from the file size, letting a still-recording file grow
7//! ([`AudioSource::extend`]): recorders write a placeholder data size until they stop.
8
9use 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
18/// What datui does with an audio file: see [`crate::formats::readers`].
19pub(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
29/// The most channels a file may declare; each is a column.
30const MAX_CHANNELS: u16 = 1024;
31/// The most chunks walked. Real files have a handful; each step moves at least 8 bytes,
32/// so this bounds a file made of nothing but empty chunks.
33const MAX_CHUNKS: usize = 1 << 16;
34/// The most markers kept from a `cue ` or `MARK` chunk.
35const MAX_MARKERS: usize = 100_000;
36/// The longest text kept from one chunk (iXML, coding history, an annotation).
37const MAX_TEXT: usize = 1 << 20;
38
39/// The container a file is.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum Container {
42    /// RIFF WAVE; with a `bext` chunk it is a Broadcast WAV.
43    Wav,
44    /// RF64 or BW64: WAVE with 64-bit sizes in a `ds64` chunk.
45    Rf64,
46    Aiff,
47    /// AIFF-C, which names its sample encoding.
48    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/// How one sample is stored.
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum Sample {
65    /// Unsigned 8-bit, offset by 128 (WAV, and AIFF-C `raw `). Shown signed.
66    U8,
67    I8,
68    I16,
69    I24,
70    I32,
71    F32,
72    F64,
73}
74
75impl Sample {
76    /// Bytes one sample takes.
77    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    /// The column's type. Integers keep an integer type wide enough for the sample
92    /// (24-bit is `Int32`); normalized, they are `Float32` in [-1, 1].
93    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    /// The magnitude of full scale: the most negative integer sample, negated, or 1.0
105    /// for float.
106    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    /// `16-bit integer`, `32-bit float`.
114    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/// A marker or region from a `cue ` (with `LIST adtl` labels) or `MARK` chunk.
121#[derive(Debug, Clone, PartialEq)]
122pub struct Marker {
123    pub id: u32,
124    /// The frame it marks.
125    pub sample: u64,
126    pub label: String,
127    /// A region's length in frames, from an `ltxt` chunk.
128    pub length: Option<u64>,
129}
130
131/// What the header says: the format, where the samples are, and everything else the
132/// Info panel shows.
133#[derive(Debug, Clone, PartialEq)]
134pub struct AudioHeader {
135    pub container: Container,
136    /// A Broadcast WAV: the file has a `bext` chunk.
137    pub broadcast: bool,
138    /// `PCM`, `IEEE float`, `extensible PCM`, or an AIFF-C encoding.
139    pub encoding: String,
140    pub sample: Sample,
141    pub big_endian: bool,
142    pub channels: u16,
143    pub sample_rate: f64,
144    /// Bits that carry the signal, when fewer than the container's (extensible, AIFF).
145    pub valid_bits: u16,
146    /// Bytes per frame: one sample per channel, in channel order, plus any padding.
147    pub frame_bytes: usize,
148    /// One name per channel: from the extensible channel mask, else `ch1..chN`.
149    pub channel_names: Vec<String>,
150    /// Where the samples start.
151    pub data_offset: u64,
152    /// The data size the header states; `None` for 0 or a placeholder, which a
153    /// recorder writes until it stops.
154    pub data_declared: Option<u64>,
155    /// Key and value, in the order found: the format, `bext`, iXML, `LIST INFO`.
156    pub metadata: Vec<(String, String)>,
157    pub markers: Vec<Marker>,
158}
159
160impl AudioHeader {
161    /// The data bytes a file of `file_len` holds: what the header declares, cut to the
162    /// file, or everything after the data offset when the header does not say.
163    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    /// Whole frames in a file of `file_len`. A partial last frame is not one.
170    pub fn frames(&self, file_len: u64) -> u64 {
171        self.data_len(file_len) / self.frame_bytes as u64
172    }
173
174    /// Where frame `frame` sits, in seconds from the start.
175    pub fn seconds(&self, frame: u64) -> f64 {
176        frame as f64 / self.sample_rate
177    }
178}
179
180/// Whether the first bytes are a WAV, RF64, BW64 or AIFF file.
181pub 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
187/// Read the header of a whole file's bytes.
188pub 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
218/// Text from a fixed field or a chunk: up to the first NUL, cut to [`MAX_TEXT`],
219/// control characters other than newline and tab dropped, trimmed.
220fn 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
232/// The name of each channel the extensible mask's bits name, in bit order.
233const 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
238/// Column names for `channels` channels: the mask's speakers in order, then `chN` for
239/// any the mask does not name.
240fn 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
251/// The GUID tail every `KSDATAFORMAT_SUBTYPE_*` shares after its format tag.
252const SUBTYPE_TAIL: [u8; 14] = [
253    0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71,
254];
255
256/// What a format tag is called, for a refusal.
257fn 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    // Plain PCM may hold each sample in a wider container than its bits round to (24
309    // bits in 4 bytes); the block alignment says so, and the bits are the high ones,
310    // as in an extensible file.
311    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
349/// Check the shape every container shares and work out the frame size.
350fn 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        // Padding after the samples is allowed; a frame too small to hold them is not.
362        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
379/// Chunks of a RIFF or AIFF file as `(id, body start, stated body length)`, stopping at
380/// the file's end, an unfit header, or [`MAX_CHUNKS`]. A body overrunning the end is
381/// passed as stated; the caller decides if it is running data or corruption.
382fn 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        // Chunks are padded to an even length. A size that overflows ends the walk on
401        // the next step, past the end.
402        pos = body.saturating_add(size).saturating_add(size & 1);
403        Some((id, body, size))
404    })
405}
406
407/// The body of a chunk that must lie inside the file.
408fn 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            // A data size of 0 or a placeholder means it runs to the end of the file:
436            // nothing after it is a chunk.
437            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            // A chunk that runs past the end: a corrupt or cut-short file. What came
446            // before it stands; a format that never arrived is the error below.
447            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    // The time reference counts samples since midnight; with the rate it is a time of
468    // day, which is what a sync to picture needs.
469    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
520/// A plain RIFF data size, which is 32 bits: a writer that runs past 4 GiB without
521/// switching to RF64 leaves the size modulo 2^32. With more than 4 GiB after the
522/// chunk's start, the true size is the largest `stated + k * 2^32` that fits.
523fn 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
530/// The Broadcast WAV fields worth reading, by their fixed offsets.
531fn 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
559/// iXML's common fields by a plain tag search, and the document itself as raw text:
560/// datui has no XML parser to spare for it.
561fn 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    // The count is checked against the points the chunk can hold, not trusted.
585    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
593/// Sub-chunks of a `LIST`: `(id, body)`, each inside `body`.
594fn 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
643/// An IEEE 754 80-bit extended float, as AIFF stores its sample rate.
644fn 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
661/// An AIFF Pascal string: a count byte then the text, padded to an even length.
662/// Returns the text and the bytes taken.
663fn 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            // 0 is a recorder's placeholder: the data runs to the end of the file.
685            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    // COMM's frame count is the authority when it is set; 0 is a recorder's placeholder.
742    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
837/// An open audio file: its header and a map of its bytes, read a window at a time.
838pub struct AudioSource {
839    /// The file the map is of, for [`Self::extend`]; `None` for bytes given whole.
840    file: Option<File>,
841    map: Mmap,
842    header: AudioHeader,
843    frames: u64,
844    /// Integer samples as `Float32` in [-1, 1].
845    normalize: bool,
846}
847
848// The in-memory engine builds the plan's frame index from row 0 up to a slice's end, so
849// the window at the end of a long recording would cost 4 bytes per frame before it.
850impl 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
866/// The name of the frame-number column.
867pub const FRAME: &str = "frame";
868/// The name of the column of each frame's time from the start, in seconds. A float
869/// rather than a Duration: it charts, compares and reads as a plain number
870/// (`12.345625`), where a Duration displays as mixed units.
871pub const SECONDS: &str = "seconds";
872/// The most frames shown: Polars counts rows in 32 bits.
873const 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        // SAFETY: the map is read-only and every read goes through a bounds-checked
879        // slice of it. A file truncated underneath an open map is the one thing this
880        // cannot guard; it is the same exposure Polars' own mapped readers have.
881        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    /// An audio file from its bytes, copied into an anonymous map: for the fuzz target,
894    /// which has bytes and no file.
895    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    /// Fails when the file is shorter than its map: reading past it ends the process
923    /// (SIGBUS), as a recording rewritten or cut while open would. Checked before each read.
924    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    /// Duration of the frames on hand, in seconds.
937    pub fn seconds(&self) -> f64 {
938        self.header.seconds(self.frames)
939    }
940
941    /// Frames the file holds past the most datui shows, `MAX_FRAMES`.
942    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    /// Bytes in the data region that are not a whole frame: a cut-short last frame.
949    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    /// Whether the header states more data than the file holds.
954    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    /// Frames `[start, start + len)` (cut to the frames on hand) as `columns`, in that
972    /// order, or every column. Only those frames' bytes are read.
973    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    /// What a column of [`Self::schema`] is.
1002    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    /// The bytes of one sample, or `None` for a frame past the ones on hand.
1016    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    /// One column for the frames `frames` names, in that order; a `None` frame, or one
1031    /// past the frames on hand, is null.
1032    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    /// The frames as a lazy frame that Polars can stream, slice and prune: decoded
1090    /// over a frame index ([`crate::formats::row_index`]), so a slice anywhere decodes only its
1091    /// own frames and a query that names one channel decodes only that channel.
1092    pub fn lazy(self: &Arc<Self>) -> LazyFrame {
1093        crate::formats::row_index::lazy(self)
1094    }
1095
1096    /// The lowest and highest value a channel's column can hold, in the column's own
1097    /// units: the most negative and most positive integer the valid bits allow, or
1098    /// [-1, 1] for float. A sample at either is at full scale.
1099    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        // Valid bits are the high ones: a 20-bit sample in a 24-bit container steps by 16.
1107        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    /// One pass over every frame, per channel, measuring clipping (runs at full scale),
1119    /// dropouts (runs of exact zeros) and DC offset (the mean), in flat memory. `stop` is
1120    /// checked every million frames; `None` when stopped.
1121    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        // Each sample as the column holds it, so the bounds and the drill-in's
1146        // predicate agree with the count: normalized, through f32 as `decode` makes it.
1147        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
1257/// An integer sample's value from its bytes. 8-bit WAV is unsigned around 128 and is
1258/// shown signed; 24-bit is sign-extended. Float samples are not integers: 0.
1259fn 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            // Sign-extended by shifting the top byte into place and back.
1278            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
1311/// The shortest run of samples at full scale that counts as clipping. One sample
1312/// there is a loud peak; three in a row is the waveform flattened against the limit.
1313pub const CLIP_RUN: u64 = 3;
1314
1315/// What [`AudioSource::signal_report`] measured of one channel.
1316#[derive(Debug, Clone, Default, PartialEq)]
1317pub struct SignalReport {
1318    pub channel: String,
1319    pub frames: u64,
1320    /// The values at or past which a sample is at full scale, in the column's units.
1321    pub full_scale: (f64, f64),
1322    /// Samples at full scale, in runs or not.
1323    pub at_full_scale: u64,
1324    /// The shortest run counted, and the runs of at least that many samples at full
1325    /// scale, the samples in them, and the longest.
1326    pub clip_run_min: u64,
1327    pub clip_runs: u64,
1328    pub in_clip_runs: u64,
1329    pub longest_clip: u64,
1330    /// The shortest run of exact zeros counted (10 ms, and at least 16 samples), and
1331    /// the runs that long, the samples in them, and the longest.
1332    pub zero_run_min: u64,
1333    pub zero_runs: u64,
1334    pub in_zero_runs: u64,
1335    pub longest_zeros: u64,
1336    /// The channel's mean, in the column's units.
1337    pub mean: f64,
1338}
1339
1340/// A column of [`AudioSource::schema`].
1341#[derive(Debug, Clone, Copy)]
1342enum Which {
1343    Frame,
1344    Seconds,
1345    Channel(usize),
1346}
1347
1348/// The recording a window of the dataset reads its rows from, when the dataset is
1349/// one: for the quality checks that read every sample (clipping, runs of zeros, DC
1350/// offset).
1351pub(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
1358/// The Audio tab: the file's format, size and length, then its metadata (`bext`, iXML,
1359/// `LIST INFO`, AIFF text) and markers, markers last.
1360pub 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        // An audio file's columns are the frame, the time and one per channel; what is
1458        // particular to it is here.
1459        first: true,
1460        own_columns: true,
1461        ..Default::default()
1462    }
1463}
1464
1465/// The scan of an audio file: its frames, read from the file where they are shown.
1466/// The source is the window the dataset reads them through, and what a full quality
1467/// run checks the signal of ([`recording`]).
1468fn 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    // The count is arithmetic on the file's size: nothing to scan for it.
1472    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;