Skip to main content

datui_lib/
audio.rs

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