Skip to main content

audio_file/reader/
mod.rs

1//! Reading audio files.
2//!
3//! Two decoders share the work: the wav fast path right here in this module,
4//! and `general`, the Symphonia-backed path for every other format, which
5//! only exists when the `symphonia` feature is on. `decode` is where the two
6//! meet - it tries the wav path first and only reaches for `general` when
7//! that declines the file.
8//!
9//! # Channel count limits
10//!
11//! Symphonia maps channels to named speaker positions rather than treating them
12//! as a plain count, so formats read through it have channel ceilings below what
13//! the format itself allows. Wav files read by the built-in decoder have no
14//! ceiling, and neither does Matroska, so prefer Matroska for high channel counts
15//! in a compressed format.
16//!
17//! - **WAV** files the built-in decoder cannot handle, so ADPCM and A-law/mu-law,
18//!   are rejected above 18 channels for an extensible `fmt ` chunk, or 26 for a
19//!   plain one.
20//! - **CAF** is unreliable above 18 channels: a 24-channel file decodes as 18
21//!   channels with misaligned samples and no error at all, and 32 channels is
22//!   rejected.
23//! - **FLAC** is capped at 8 channels by the format itself.
24
25use std::fs::File;
26use std::path::Path;
27
28use num_traits::Float;
29use thiserror::Error;
30
31#[cfg(feature = "resample")]
32use crate::resample::{ResampleError, resample};
33
34#[cfg(feature = "symphonia")]
35mod general;
36
37/// What [`read`] needs of a sample type beyond being a float, which is whatever
38/// the resampler needs of it. With the `resample` feature that is
39/// `rubato::Sample`.
40#[cfg(feature = "resample")]
41pub use rubato::Sample as ResampleSample;
42
43/// What [`read`] needs of a sample type beyond being a float. Without the
44/// `resample` feature nothing is resampled, so this asks for nothing and every
45/// type satisfies it. It exists so that the bound on [`read`] reads the same in
46/// either build.
47#[cfg(not(feature = "resample"))]
48pub trait ResampleSample {}
49
50#[cfg(not(feature = "resample"))]
51impl<F> ResampleSample for F {}
52
53/// Audio data with interleaved samples
54#[derive(Debug, Clone)]
55pub struct Audio<F> {
56    /// Interleaved audio samples
57    pub samples_interleaved: Vec<F>,
58    /// Sample rate in Hz
59    pub sample_rate: u32,
60    /// Number of channels
61    pub num_channels: u16,
62}
63
64#[derive(Debug, Error)]
65#[non_exhaustive]
66pub enum ReadError {
67    #[error("could not read file: {0}")]
68    Io(#[from] std::io::Error),
69
70    /// A packet the decoder rejected. The file is damaged, truncated, or in an
71    /// encoding this build cannot decode. Nothing is skipped over: a file that
72    /// cannot be decoded in full is not read at all.
73    #[cfg(feature = "symphonia")]
74    #[error("could not decode audio: {0}")]
75    Decode(#[from] symphonia::core::errors::Error),
76
77    /// No decoder in this build can read the file. Only reachable without the
78    /// `symphonia` feature, where the built-in wav decoder is the whole reader
79    /// and everything it does not recognize has nowhere left to go.
80    #[error("no decoder in this build can read this file")]
81    UnsupportedFormat,
82
83    #[error("no track found")]
84    NoTrack,
85
86    #[error("no sample rate found")]
87    NoSampleRate,
88
89    #[error("could not determine the number of channels")]
90    NoChannels,
91
92    #[error("channel count ({0}) exceeds the supported maximum of 65535")]
93    TooManyChannels(usize),
94
95    #[error("start frame ({start}) must not exceed end frame ({end})")]
96    InvalidFrameRange { start: usize, end: usize },
97
98    #[error("start channel {start} out of bounds (file has {total} channels)")]
99    InvalidStartChannel { start: usize, total: usize },
100
101    #[error("channel count must not be zero")]
102    ZeroChannels,
103
104    #[error(
105        "channel range out of bounds: {count} channels starting at channel {start} (file has {total} channels)"
106    )]
107    InvalidChannelRange {
108        start: usize,
109        count: usize,
110        total: usize,
111    },
112
113    #[error("channel count changed mid-stream (was {expected}, now {found})")]
114    ChannelCountChanged { expected: usize, found: usize },
115
116    #[error("sample rate changed mid-stream (was {expected}, now {found})")]
117    SampleRateChanged { expected: u32, found: u32 },
118
119    /// Frames the stream never delivered, which the read cannot leave out
120    /// without moving every later frame off the position it was asked for.
121    #[cfg(feature = "symphonia")]
122    #[error("frames {start}..{end} are missing, the file is damaged or incomplete")]
123    MissingFrames { start: u64, end: u64 },
124
125    #[cfg(feature = "resample")]
126    #[error("resample failed")]
127    Resample(#[from] ResampleError),
128}
129
130/// Position in the audio stream (for start or stop points)
131#[derive(Default, Debug, Clone, Copy)]
132pub enum Position {
133    /// Start from beginning or read until the end (depending on context)
134    #[default]
135    Default,
136    /// Specific time offset
137    Time(std::time::Duration),
138    /// Specific frame number (sample position across all channels)
139    Frame(usize),
140}
141
142#[derive(Default)]
143pub struct ReadConfig {
144    /// Where to start reading audio (time or frame-based), inclusive
145    pub start: Position,
146    /// Where to stop reading audio (time or frame-based), exclusive
147    pub stop: Position,
148    /// Starting channel to extract (0-indexed). None means start from channel 0.
149    pub start_channel: Option<usize>,
150    /// Number of channels to extract. None means extract all remaining channels.
151    pub num_channels: Option<usize>,
152    /// If specified the audio will be resampled to the given sample rate.
153    ///
154    /// Only present with the `resample` feature, so that a build without it
155    /// cannot ask for a rate that nothing would resample to.
156    #[cfg(feature = "resample")]
157    pub sample_rate: Option<u32>,
158}
159
160/// Upper bound for the pre-allocation derived from the container metadata, so
161/// that a bogus frame count cannot request a huge allocation up front. The
162/// buffer still grows beyond this if the file really is that long.
163///
164/// Both decoding paths respect it, so a hostile header costs the same either
165/// way.
166pub(crate) const MAX_PREALLOC_SAMPLES: usize = 16 * 1024 * 1024;
167
168/// Read an audio file from disk.
169///
170/// Only the selected range is decoded and stored. `F` is the sample type of the
171/// returned audio, either `f32` or `f64`, with full scale at `±1.0`.
172///
173/// No gain is applied anywhere: integer samples are divided by the full scale of
174/// their bit depth, and float samples pass through as they are stored, so a file
175/// written with samples beyond `±1.0` still reads back beyond `±1.0`.
176///
177/// The `stop` position of [`ReadConfig`] is exclusive, so reading from frame 100
178/// to frame 200 yields 100 frames. A `start` position beyond the end of the file
179/// yields no samples.
180pub fn read<F: Float + ResampleSample>(
181    path: impl AsRef<Path>,
182    config: ReadConfig,
183) -> Result<Audio<F>, ReadError> {
184    let decoded = decode::<F>(path.as_ref(), &config)?;
185    let num_channels = checked_num_channels(decoded.num_channels)?;
186    let (samples, sample_rate) = resolve_output_rate(decoded, &config)?;
187
188    Ok(Audio {
189        samples_interleaved: samples,
190        sample_rate,
191        num_channels,
192    })
193}
194
195/// Resample to the rate requested in `config`, if any and if it differs from
196/// the decoded rate. Without the `resample` feature there is no rate to
197/// resample to, so the decoded audio passes through unchanged.
198#[cfg(feature = "resample")]
199fn resolve_output_rate<F: Float + ResampleSample>(
200    decoded: Decoded<F>,
201    config: &ReadConfig,
202) -> Result<(Vec<F>, u32), ReadError> {
203    Ok(match config.sample_rate {
204        Some(sr_out) if sr_out != decoded.sample_rate => (
205            resample(
206                &decoded.samples,
207                decoded.num_channels,
208                decoded.sample_rate,
209                sr_out,
210            )?,
211            sr_out,
212        ),
213        _ => (decoded.samples, decoded.sample_rate),
214    })
215}
216
217#[cfg(not(feature = "resample"))]
218fn resolve_output_rate<F>(
219    decoded: Decoded<F>,
220    _config: &ReadConfig,
221) -> Result<(Vec<F>, u32), ReadError> {
222    Ok((decoded.samples, decoded.sample_rate))
223}
224
225/// The channel count is reported as a `u16`, so a stream with more channels than
226/// that cannot be described by [`Audio`].
227fn checked_num_channels(count: usize) -> Result<u16, ReadError> {
228    u16::try_from(count).map_err(|_| ReadError::TooManyChannels(count))
229}
230
231/// Decoded audio at the sample rate of the file, before any resampling.
232struct Decoded<F> {
233    samples: Vec<F>,
234    num_channels: usize,
235    sample_rate: u32,
236}
237
238/// Channel layout of the decoded stream, resolved against the read config.
239#[derive(Clone, Copy)]
240struct Layout {
241    /// Channels per frame in the file.
242    ///
243    /// Only the general decoder needs it, to stride across a decoded packet.
244    /// The wav path indexes the selected channels straight out of the file.
245    #[cfg_attr(not(feature = "symphonia"), allow(dead_code))]
246    total: usize,
247    /// First channel to extract
248    start: usize,
249    /// Number of channels to extract
250    count: usize,
251}
252
253/// Everything about a read that can only be resolved once the audio
254/// specification is known: the frame positions depend on the sample rate, and
255/// the channel selection on the channel count.
256///
257/// It is resolved from the first packet that decodes, because container metadata
258/// can contradict the bitstream headers. A Matroska `SamplingFrequency` element
259/// may disagree with the FLAC stream info it wraps, symphonia's demuxers report
260/// the container value, and only some decoders amend their codec parameters with
261/// what they read from the bitstream. The specification of decoded audio is
262/// therefore the only reliable source, and the declared one is used only for
263/// files without a single decodable packet.
264#[derive(Clone, Copy)]
265struct Plan {
266    sample_rate: u32,
267    layout: Layout,
268    /// First frame to copy, inclusive
269    start_frame: usize,
270    /// Frame to stop before, if the read is bounded
271    end_frame: Option<usize>,
272}
273
274impl Plan {
275    /// Resolve and validate the read config against an audio specification.
276    fn resolve(sample_rate: u32, channels: usize, config: &ReadConfig) -> Result<Self, ReadError> {
277        let start_frame = position_to_frame(config.start, sample_rate).unwrap_or(0);
278        let end_frame = position_to_frame(config.stop, sample_rate);
279
280        if let Some(end_frame) = end_frame
281            && start_frame > end_frame
282        {
283            return Err(ReadError::InvalidFrameRange {
284                start: start_frame,
285                end: end_frame,
286            });
287        }
288
289        let (start, count) = channel_range(config, channels)?;
290        // `Audio` reports the channel count as a `u16`, so a selection it cannot
291        // describe is rejected here instead of after the whole file is decoded.
292        checked_num_channels(count)?;
293
294        Ok(Self {
295            sample_rate,
296            layout: Layout {
297                total: channels,
298                start,
299                count,
300            },
301            start_frame,
302            end_frame,
303        })
304    }
305}
306
307/// Attempts the WAV fast path: parsing the header directly and reading only
308/// the requested bytes. PCM audio in a WAV file is a flat byte array, so a
309/// frame range and a channel range are read by indexing into it directly,
310/// with none of the packet timestamps, decoder warm-up or seek verification
311/// the general path below needs for compressed formats.
312///
313/// Returns `Ok(None)` for anything the native decoder does not handle - a
314/// file that is not WAV, or a WAV sample encoding it does not decode, such
315/// as ADPCM - so the caller falls back to the general path.
316fn try_native_wav<F: Float>(
317    path: &Path,
318    config: &ReadConfig,
319) -> Result<Option<Decoded<F>>, ReadError> {
320    let file = File::open(path)?;
321    let Some(wav) = crate::wav::open_wav(file)? else {
322        return Ok(None);
323    };
324
325    let plan = Plan::resolve(wav.sample_rate, wav.num_channels, config)?;
326    let samples = crate::wav::read_frames::<F>(
327        wav,
328        plan.start_frame,
329        plan.end_frame,
330        plan.layout.start,
331        plan.layout.count,
332    )?;
333
334    Ok(Some(Decoded {
335        samples,
336        num_channels: plan.layout.count,
337        sample_rate: plan.sample_rate,
338    }))
339}
340
341fn decode<F: Float>(path: &Path, config: &ReadConfig) -> Result<Decoded<F>, ReadError> {
342    if let Some(decoded) = try_native_wav(path, config)? {
343        return Ok(decoded);
344    }
345
346    // Without Symphonia the wav fast path is the whole reader, so declining a
347    // file is the end of the line rather than a handover.
348    #[cfg(not(feature = "symphonia"))]
349    {
350        Err(ReadError::UnsupportedFormat)
351    }
352    #[cfg(feature = "symphonia")]
353    {
354        general::decode_with_symphonia(path, config)
355    }
356}
357
358/// Resolve and validate the requested channel range against a file with `total` channels.
359fn channel_range(config: &ReadConfig, total: usize) -> Result<(usize, usize), ReadError> {
360    let start = config.start_channel.unwrap_or(0);
361    if start >= total {
362        return Err(ReadError::InvalidStartChannel { start, total });
363    }
364
365    let count = config.num_channels.unwrap_or(total - start);
366    if count == 0 {
367        return Err(ReadError::ZeroChannels);
368    }
369    // The end of the range is only needed for this comparison, and a requested
370    // count close to `usize::MAX` would overflow while calculating it.
371    if start.checked_add(count).is_none_or(|end| end > total) {
372        return Err(ReadError::InvalidChannelRange {
373            start,
374            count,
375            total,
376        });
377    }
378
379    Ok((start, count))
380}
381
382fn position_to_frame(position: Position, sample_rate: u32) -> Option<usize> {
383    match position {
384        Position::Default => None,
385        Position::Time(duration) => {
386            Some((duration.as_secs_f64() * sample_rate as f64).round() as usize)
387        }
388        Position::Frame(frame) => Some(frame),
389    }
390}
391
392#[cfg(feature = "audio-blocks")]
393pub fn read_block<F: num_traits::Float + 'static + ResampleSample>(
394    path: impl AsRef<Path>,
395    config: ReadConfig,
396) -> Result<(audio_blocks::Interleaved<F>, u32), ReadError> {
397    let audio = read(path, config)?;
398    Ok((
399        audio_blocks::Interleaved::from_slice(&audio.samples_interleaved, audio.num_channels),
400        audio.sample_rate,
401    ))
402}
403
404#[cfg(test)]
405mod tests {
406    use super::*;
407
408    // Reading a WAV fixture needs no feature at all: the built-in decoder
409    // handles integer PCM and IEEE float in every build. Everything in this
410    // module tests that path; the Symphonia-backed path has its own tests in
411    // `general`.
412    use audio_blocks::{AudioBlock, InterleavedView};
413    use std::time::Duration;
414
415    fn to_block<F: num_traits::Float + 'static>(audio: &Audio<F>) -> InterleavedView<'_, F> {
416        InterleavedView::from_slice(&audio.samples_interleaved, audio.num_channels)
417    }
418
419    /// A file that does not exist has to surface as an I/O error instead of a
420    /// panic, whatever decoders the build contains.
421    #[test]
422    fn test_missing_file_is_reported() {
423        match read::<f32>(crate::tmp_path("does-not-exist.wav"), ReadConfig::default()) {
424            Err(ReadError::Io(e)) => assert_eq!(e.kind(), std::io::ErrorKind::NotFound),
425            other => panic!("{:?}", other.map(|audio| audio.num_channels)),
426        }
427    }
428
429    /// Without Symphonia the WAV decoder is the whole reader, so a file it does
430    /// not recognize has nowhere left to go. That has to be said plainly rather
431    /// than surfacing as a missing track or a decode failure.
432    #[cfg(not(feature = "symphonia"))]
433    #[test]
434    fn test_unsupported_format_without_the_general_decoder() {
435        match read::<f32>("test_data/test_mp3.mp3", ReadConfig::default()) {
436            Err(ReadError::UnsupportedFormat) => (),
437            other => panic!("{:?}", other.map(|audio| audio.num_channels)),
438        }
439    }
440
441    /// The point of making Symphonia optional: writing a WAV file and reading it
442    /// back is the whole job in a build without it, so it has to be covered
443    /// there and not only in the configurations that pull Symphonia in.
444    #[test]
445    fn test_wav_round_trips_without_the_general_decoder() {
446        use crate::writer::{SampleFormat, WriteConfig, write};
447
448        let path = crate::tmp_path("no-symphonia-round-trip.wav");
449        let samples: Vec<f32> = (0..96).map(|i| (i as f32 / 48.0) - 1.0).collect();
450
451        for sample_format in [
452            SampleFormat::Int16,
453            SampleFormat::Int32,
454            SampleFormat::Float32,
455        ] {
456            write(&path, &samples, 3, 48_000, WriteConfig { sample_format }).unwrap();
457
458            let audio = read::<f32>(&path, ReadConfig::default()).unwrap();
459            assert_eq!(audio.num_channels, 3, "{sample_format:?}");
460            assert_eq!(audio.sample_rate, 48_000, "{sample_format:?}");
461            approx::assert_abs_diff_eq!(
462                samples.as_slice(),
463                audio.samples_interleaved.as_slice(),
464                epsilon = 1e-4
465            );
466
467            // And a frame plus channel selection out of the middle of it.
468            let audio = read::<f32>(
469                &path,
470                ReadConfig {
471                    start: Position::Frame(4),
472                    stop: Position::Frame(9),
473                    start_channel: Some(1),
474                    num_channels: Some(2),
475                    #[cfg(feature = "resample")]
476                    sample_rate: None,
477                },
478            )
479            .unwrap();
480            assert_eq!(audio.num_channels, 2, "{sample_format:?}");
481            let src = samples.as_slice();
482            let expected: Vec<f32> = (4..9)
483                .flat_map(|frame| (1..3).map(move |ch| src[frame * 3 + ch]))
484                .collect();
485            approx::assert_abs_diff_eq!(
486                expected.as_slice(),
487                audio.samples_interleaved.as_slice(),
488                epsilon = 1e-4
489            );
490        }
491
492        std::fs::remove_file(&path).unwrap();
493    }
494
495    /// Verify that the read audio data matches the expected sine wave values.
496    /// The test file was generated by utils/generate_wav.py with these parameters:
497    /// - 4 channels with frequencies: [440, 554.37, 659.25, 880] Hz
498    /// - Sample rate: 48000 Hz
499    /// - Duration: 1 second (48000 samples)
500    #[test]
501    fn test_sine_wave_data_integrity() {
502        const SAMPLE_RATE: f64 = 48000.0;
503        const N_SAMPLES: usize = 48000;
504        const FREQUENCIES: [f64; 4] = [440.0, 554.37, 659.25, 880.0];
505
506        let audio = read::<f32>("test_data/test_4ch.wav", ReadConfig::default()).unwrap();
507        let block = to_block(&audio);
508
509        assert_eq!(audio.sample_rate, 48000);
510        assert_eq!(block.num_frames(), N_SAMPLES);
511        assert_eq!(block.num_channels(), 4);
512
513        // Verify each channel contains the expected sine wave
514        for (ch, &freq) in FREQUENCIES.iter().enumerate() {
515            for frame in 0..N_SAMPLES {
516                let expected =
517                    (2.0 * std::f64::consts::PI * freq * frame as f64 / SAMPLE_RATE).sin() as f32;
518                let actual = block.sample(ch as u16, frame);
519                assert!(
520                    (actual - expected).abs() < 1e-4,
521                    "Mismatch at channel {ch}, frame {frame}: expected {expected}, got {actual}"
522                );
523            }
524        }
525
526        // Also verify reading with an offset works consistently
527        let audio = read::<f32>(
528            "test_data/test_4ch.wav",
529            ReadConfig {
530                start: Position::Frame(24000),
531                stop: Position::Frame(24100),
532                ..Default::default()
533            },
534        )
535        .unwrap();
536        let block = to_block(&audio);
537
538        for (ch, &freq) in FREQUENCIES.iter().enumerate() {
539            for frame in 0..100 {
540                let actual_frame = 24000 + frame;
541                let expected = (2.0 * std::f64::consts::PI * freq * actual_frame as f64
542                    / SAMPLE_RATE)
543                    .sin() as f32;
544                let actual = block.sample(ch as u16, frame);
545                assert!(
546                    (actual - expected).abs() < 1e-4,
547                    "Offset mismatch at channel {ch}, frame {actual_frame}: expected {expected}, got {actual}"
548                );
549            }
550        }
551    }
552
553    #[test]
554    fn test_samples_selection() {
555        let audio1 = read::<f32>("test_data/test_1ch.wav", ReadConfig::default()).unwrap();
556        let block1 = to_block(&audio1);
557        assert_eq!(audio1.sample_rate, 48000);
558        assert_eq!(block1.num_frames(), 48000);
559        assert_eq!(block1.num_channels(), 1);
560
561        let audio2 = read::<f32>(
562            "test_data/test_1ch.wav",
563            ReadConfig {
564                start: Position::Frame(1100),
565                stop: Position::Frame(1200),
566                ..Default::default()
567            },
568        )
569        .unwrap();
570        let block2 = to_block(&audio2);
571        assert_eq!(audio2.sample_rate, 48000);
572        assert_eq!(block2.num_frames(), 100);
573        assert_eq!(block2.num_channels(), 1);
574        assert_eq!(block1.raw_data()[1100..1200], block2.raw_data()[..]);
575    }
576
577    #[test]
578    fn test_time_selection() {
579        let audio1 = read::<f32>("test_data/test_1ch.wav", ReadConfig::default()).unwrap();
580        let block1 = to_block(&audio1);
581        assert_eq!(audio1.sample_rate, 48000);
582        assert_eq!(block1.num_frames(), 48000);
583        assert_eq!(block1.num_channels(), 1);
584
585        let audio2 = read::<f32>(
586            "test_data/test_1ch.wav",
587            ReadConfig {
588                start: Position::Time(Duration::from_secs_f32(0.5)),
589                stop: Position::Time(Duration::from_secs_f32(0.6)),
590                ..Default::default()
591            },
592        )
593        .unwrap();
594        let block2 = to_block(&audio2);
595
596        assert_eq!(audio2.sample_rate, 48000);
597        assert_eq!(block2.num_frames(), 4800);
598        assert_eq!(block2.num_channels(), 1);
599        assert_eq!(block1.raw_data()[24000..28800], block2.raw_data()[..]);
600    }
601
602    #[test]
603    fn test_channel_selection() {
604        let audio1 = read::<f32>("test_data/test_4ch.wav", ReadConfig::default()).unwrap();
605        let block1 = to_block(&audio1);
606        assert_eq!(audio1.sample_rate, 48000);
607        assert_eq!(block1.num_frames(), 48000);
608        assert_eq!(block1.num_channels(), 4);
609
610        let audio2 = read::<f32>(
611            "test_data/test_4ch.wav",
612            ReadConfig {
613                start_channel: Some(1),
614                num_channels: Some(2),
615                ..Default::default()
616            },
617        )
618        .unwrap();
619        let block2 = to_block(&audio2);
620
621        assert_eq!(audio2.sample_rate, 48000);
622        assert_eq!(block2.num_frames(), 48000);
623        assert_eq!(block2.num_channels(), 2);
624
625        // Verify we extracted channels 1 and 2 (skipping channel 0 and 3)
626        for frame in 0..10 {
627            assert_eq!(block2.sample(0, frame), block1.sample(1, frame));
628            assert_eq!(block2.sample(1, frame), block1.sample(2, frame));
629        }
630    }
631
632    #[test]
633    fn test_fail_selection() {
634        match read::<f32>(
635            "test_data/test_1ch.wav",
636            ReadConfig {
637                start: Position::Frame(100),
638                stop: Position::Frame(99),
639                ..Default::default()
640            },
641        ) {
642            Err(ReadError::InvalidFrameRange { start: _, end: _ }) => (),
643            _ => panic!(),
644        }
645
646        match read::<f32>(
647            "test_data/test_1ch.wav",
648            ReadConfig {
649                start: Position::Time(Duration::from_secs_f32(0.6)),
650                stop: Position::Time(Duration::from_secs_f32(0.5)),
651                ..Default::default()
652            },
653        ) {
654            Err(ReadError::InvalidFrameRange { start: _, end: _ }) => (),
655            _ => panic!(),
656        }
657
658        match read::<f32>(
659            "test_data/test_1ch.wav",
660            ReadConfig {
661                start_channel: Some(1),
662                ..Default::default()
663            },
664        ) {
665            Err(ReadError::InvalidStartChannel { start: _, total: _ }) => (),
666            _ => panic!(),
667        }
668
669        // A start channel beyond the channel count must not overflow while
670        // defaulting the channel count to "all remaining channels"
671        match read::<f32>(
672            "test_data/test_1ch.wav",
673            ReadConfig {
674                start_channel: Some(3),
675                ..Default::default()
676            },
677        ) {
678            Err(ReadError::InvalidStartChannel { start: 3, total: 1 }) => (),
679            other => panic!("{other:?}"),
680        }
681
682        match read::<f32>(
683            "test_data/test_1ch.wav",
684            ReadConfig {
685                num_channels: Some(0),
686                ..Default::default()
687            },
688        ) {
689            Err(ReadError::ZeroChannels) => (),
690            _ => panic!(),
691        }
692
693        match read::<f32>(
694            "test_data/test_1ch.wav",
695            ReadConfig {
696                num_channels: Some(2),
697                ..Default::default()
698            },
699        ) {
700            Err(ReadError::InvalidChannelRange {
701                start: 0,
702                count: 2,
703                total: 1,
704            }) => (),
705            other => panic!("{other:?}"),
706        }
707
708        // A channel count that overflows the end of the range must be reported
709        // instead of overflowing while validating or formatting it
710        let error = read::<f32>(
711            "test_data/test_4ch.wav",
712            ReadConfig {
713                start_channel: Some(1),
714                num_channels: Some(usize::MAX),
715                ..Default::default()
716            },
717        )
718        .expect_err("a channel count of usize::MAX must be rejected");
719
720        assert!(
721            matches!(
722                error,
723                ReadError::InvalidChannelRange {
724                    start: 1,
725                    count: usize::MAX,
726                    total: 4,
727                }
728            ),
729            "{error:?}"
730        );
731        assert!(!error.to_string().is_empty());
732    }
733
734    #[cfg(feature = "resample")]
735    #[test]
736    fn test_resample_preserves_frequency() {
737        const FREQUENCIES: [f64; 4] = [440.0, 554.37, 659.25, 880.0];
738        let sr_out: u32 = 22050;
739
740        // Read and resample in one step
741        let audio = read::<f32>(
742            "test_data/test_4ch.wav",
743            ReadConfig {
744                sample_rate: Some(sr_out),
745                ..Default::default()
746            },
747        )
748        .unwrap();
749        let block = to_block(&audio);
750
751        assert_eq!(audio.sample_rate, sr_out); // Resampled sample rate is returned
752        assert_eq!(block.num_channels(), 4);
753
754        // Expected frames after resampling: 48000 * (22050/48000) = 22050
755        let expected_frames = 22050;
756        assert_eq!(
757            block.num_frames(),
758            expected_frames,
759            "Expected {} frames, got {}",
760            expected_frames,
761            block.num_frames()
762        );
763
764        // Verify sine wave frequencies are preserved after resampling
765        // Skip first ~100 samples to avoid any edge effects from resampling
766        let start_frame = 100;
767        let test_frames = 1000;
768
769        for (ch, &freq) in FREQUENCIES.iter().enumerate() {
770            let mut max_error: f32 = 0.0;
771            for frame in start_frame..(start_frame + test_frames) {
772                let expected =
773                    (2.0 * std::f64::consts::PI * freq * frame as f64 / sr_out as f64).sin() as f32;
774                let actual = block.sample(ch as u16, frame);
775                let error = (actual - expected).abs();
776                max_error = max_error.max(error);
777            }
778            assert!(
779                max_error < 0.02,
780                "Channel {} ({}Hz): max error {} exceeds threshold",
781                ch,
782                freq,
783                max_error
784            );
785        }
786    }
787
788    #[cfg(feature = "resample")]
789    #[test]
790    fn test_channel_selection_with_resampling() {
791        // This test verifies that channel selection combined with resampling works correctly
792        const FREQUENCIES: [f64; 4] = [440.0, 554.37, 659.25, 880.0];
793        let sr_out: u32 = 22050;
794
795        // Read channels 1 and 2 (indices 1 and 2) with resampling
796        let audio = read::<f32>(
797            "test_data/test_4ch.wav",
798            ReadConfig {
799                start_channel: Some(1),
800                num_channels: Some(2),
801                sample_rate: Some(sr_out),
802                ..Default::default()
803            },
804        )
805        .unwrap();
806        let block = to_block(&audio);
807
808        assert_eq!(audio.num_channels, 2, "Should have 2 channels");
809        assert_eq!(
810            audio.sample_rate, sr_out,
811            "Sample rate should be the resampled rate"
812        );
813
814        // Expected frames after resampling: 48000 * (22050/48000) = 22050
815        let expected_frames = 22050;
816        assert_eq!(
817            block.num_frames(),
818            expected_frames,
819            "Expected {} frames, got {}",
820            expected_frames,
821            block.num_frames()
822        );
823
824        // Verify that the resampled audio contains the correct frequencies
825        // Channels 1 and 2 should have frequencies 554.37 Hz and 659.25 Hz
826        let selected_freqs = &FREQUENCIES[1..3];
827
828        let start_frame = 100;
829        let test_frames = 1000;
830
831        for (ch, &freq) in selected_freqs.iter().enumerate() {
832            let mut max_error: f32 = 0.0;
833            for frame in start_frame..(start_frame + test_frames) {
834                let expected =
835                    (2.0 * std::f64::consts::PI * freq * frame as f64 / sr_out as f64).sin() as f32;
836                let actual = block.sample(ch as u16, frame);
837                let error = (actual - expected).abs();
838                max_error = max_error.max(error);
839            }
840            assert!(
841                max_error < 0.02,
842                "Channel {} ({}Hz): max error {} exceeds threshold",
843                ch,
844                freq,
845                max_error
846            );
847        }
848    }
849
850    #[test]
851    fn test_channel_count_must_fit_the_reported_type() {
852        assert_eq!(checked_num_channels(2).unwrap(), 2);
853        assert_eq!(
854            checked_num_channels(usize::from(u16::MAX)).unwrap(),
855            u16::MAX
856        );
857
858        let error = checked_num_channels(usize::from(u16::MAX) + 1)
859            .expect_err("more channels than u16 can hold must be rejected");
860        assert!(
861            matches!(error, ReadError::TooManyChannels(65_536)),
862            "{error:?}"
863        );
864        assert!(!error.to_string().is_empty());
865
866        // The same limit is enforced when the read plan is resolved, before any
867        // decoding happens
868        assert!(matches!(
869            Plan::resolve(48_000, 65_536, &ReadConfig::default()),
870            Err(ReadError::TooManyChannels(65_536))
871        ));
872        // ... while selecting fewer channels than the file has stays allowed
873        let plan = Plan::resolve(
874            48_000,
875            65_536,
876            &ReadConfig {
877                num_channels: Some(2),
878                ..Default::default()
879            },
880        )
881        .unwrap();
882        assert_eq!(plan.layout.count, 2);
883    }
884
885    #[test]
886    fn test_plan_is_resolved_against_the_given_specification() {
887        let config = ReadConfig {
888            start: Position::Time(std::time::Duration::from_millis(10)),
889            stop: Position::Time(std::time::Duration::from_millis(20)),
890            start_channel: Some(1),
891            num_channels: Some(2),
892            #[cfg(feature = "resample")]
893            sample_rate: None,
894        };
895
896        // Frame positions follow the given sample rate, not the config
897        let plan = Plan::resolve(48_000, 4, &config).unwrap();
898        assert_eq!(plan.sample_rate, 48_000);
899        assert_eq!(plan.start_frame, 480);
900        assert_eq!(plan.end_frame, Some(960));
901        assert_eq!(plan.layout.total, 4);
902        assert_eq!(plan.layout.start, 1);
903        assert_eq!(plan.layout.count, 2);
904
905        let plan = Plan::resolve(24_000, 4, &config).unwrap();
906        assert_eq!(plan.start_frame, 240);
907        assert_eq!(plan.end_frame, Some(480));
908
909        // The channel selection is validated against the given channel count
910        assert!(matches!(
911            Plan::resolve(48_000, 2, &config),
912            Err(ReadError::InvalidChannelRange {
913                start: 1,
914                count: 2,
915                total: 2
916            })
917        ));
918
919        let backwards = ReadConfig {
920            start: Position::Frame(100),
921            stop: Position::Frame(99),
922            ..Default::default()
923        };
924        assert!(matches!(
925            Plan::resolve(48_000, 1, &backwards),
926            Err(ReadError::InvalidFrameRange {
927                start: 100,
928                end: 99
929            })
930        ));
931    }
932
933    /// A stop position must not bypass the resampling step.
934    #[cfg(feature = "resample")]
935    #[test]
936    fn test_stop_with_resampling() {
937        let sr_out: u32 = 24000;
938
939        let audio = read::<f32>(
940            "test_data/test_4ch.wav",
941            ReadConfig {
942                stop: Position::Frame(24000),
943                sample_rate: Some(sr_out),
944                ..Default::default()
945            },
946        )
947        .unwrap();
948
949        assert_eq!(audio.sample_rate, sr_out);
950        assert_eq!(audio.num_channels, 4);
951        // 24000 frames at 48 kHz are 12000 frames at 24 kHz
952        assert_eq!(to_block(&audio).num_frames(), 12000);
953    }
954
955    /// The seek path is only taken for start offsets of more than one second.
956    #[test]
957    fn test_start_beyond_seek_threshold() {
958        let path = crate::tmp_path("read-seek.wav");
959
960        // Three seconds of a ramp, so that every frame is identifiable
961        let num_frames = 48000 * 3;
962        let mut samples = Vec::with_capacity(num_frames * 2);
963        for frame in 0..num_frames {
964            let value = frame as f32 / num_frames as f32;
965            samples.push(value);
966            samples.push(-value);
967        }
968        crate::writer::write(
969            &path,
970            &samples,
971            2,
972            48000,
973            crate::writer::WriteConfig {
974                sample_format: crate::writer::SampleFormat::Float32,
975            },
976        )
977        .unwrap();
978
979        for start in [48_001, 60_000, 100_000, 143_000] {
980            let audio = read::<f32>(
981                &path,
982                ReadConfig {
983                    start: Position::Frame(start),
984                    ..Default::default()
985                },
986            )
987            .unwrap();
988
989            assert_eq!(audio.num_channels, 2);
990            assert_eq!(
991                audio.samples_interleaved.len(),
992                (num_frames - start) * 2,
993                "wrong length for start frame {start}"
994            );
995            assert_eq!(
996                audio.samples_interleaved[..2],
997                samples[start * 2..start * 2 + 2],
998                "wrong first frame for start frame {start}"
999            );
1000        }
1001
1002        std::fs::remove_file(&path).unwrap();
1003    }
1004
1005    /// What `read` documents about positions the file does not reach: a start
1006    /// beyond the end yields no samples, and a stop beyond the end clips to the
1007    /// frames that are there. Starts on both sides of the seek threshold are
1008    /// covered, because an unreachable start is what makes a seek fail.
1009    #[test]
1010    fn test_positions_beyond_the_end_of_the_file() {
1011        let path = crate::tmp_path("read-beyond-eof.wav");
1012
1013        // Half a second, so that a start beyond the end can still be below the
1014        // one second seek threshold
1015        let num_frames = 24_000;
1016        let samples: Vec<f32> = (0..num_frames * 2).map(|i| i as f32 / 1e6).collect();
1017        crate::writer::write(
1018            &path,
1019            &samples,
1020            2,
1021            48000,
1022            crate::writer::WriteConfig {
1023                sample_format: crate::writer::SampleFormat::Float32,
1024            },
1025        )
1026        .unwrap();
1027
1028        // One start per path to the same empty result: below the seek threshold
1029        // nothing is seeked, at 48_001 the seek aims one second earlier and lands
1030        // at the beginning of the file, and beyond the file the seek target
1031        // itself is out of range, so the read falls back to decoding.
1032        for start in [30_000, 48_001, 1_000_000] {
1033            let audio = read::<f32>(
1034                &path,
1035                ReadConfig {
1036                    start: Position::Frame(start),
1037                    ..Default::default()
1038                },
1039            )
1040            .unwrap();
1041
1042            assert_eq!(audio.num_channels, 2, "start frame {start}");
1043            assert_eq!(audio.sample_rate, 48000, "start frame {start}");
1044            assert!(
1045                audio.samples_interleaved.is_empty(),
1046                "start frame {start} returned {} samples",
1047                audio.samples_interleaved.len()
1048            );
1049        }
1050
1051        // Nothing to resample, but the requested rate is still what the empty
1052        // audio is labelled with
1053        #[cfg(feature = "resample")]
1054        {
1055            let audio = read::<f32>(
1056                &path,
1057                ReadConfig {
1058                    start: Position::Frame(1_000_000),
1059                    sample_rate: Some(24_000),
1060                    ..Default::default()
1061                },
1062            )
1063            .unwrap();
1064            assert_eq!(audio.sample_rate, 24_000);
1065            assert!(audio.samples_interleaved.is_empty());
1066        }
1067
1068        let audio = read::<f32>(
1069            &path,
1070            ReadConfig {
1071                stop: Position::Frame(1_000_000),
1072                ..Default::default()
1073            },
1074        )
1075        .unwrap();
1076        assert_eq!(audio.samples_interleaved, samples);
1077
1078        let audio = read::<f32>(
1079            &path,
1080            ReadConfig {
1081                stop: Position::Time(Duration::from_secs(60)),
1082                ..Default::default()
1083            },
1084        )
1085        .unwrap();
1086        assert_eq!(audio.samples_interleaved, samples);
1087
1088        std::fs::remove_file(&path).unwrap();
1089    }
1090
1091    /// A time position that does not land on a frame boundary is rounded to the
1092    /// nearest frame instead of truncated, so that a position derived from a
1093    /// frame index does not move a frame earlier.
1094    #[test]
1095    fn test_sub_frame_time_positions_are_rounded() {
1096        // 1000.4 frames at 48 kHz
1097        let start = Duration::from_nanos(20_841_666);
1098        // 1200.5 frames, which truncation would place at 1200
1099        let stop = Duration::from_nanos(25_010_417);
1100
1101        let audio = read::<f32>(
1102            "test_data/test_1ch.wav",
1103            ReadConfig {
1104                start: Position::Time(start),
1105                stop: Position::Time(stop),
1106                ..Default::default()
1107            },
1108        )
1109        .unwrap();
1110
1111        let full = read::<f32>("test_data/test_1ch.wav", ReadConfig::default()).unwrap();
1112        assert_eq!(audio.samples_interleaved.len(), 201);
1113        assert_eq!(
1114            audio.samples_interleaved,
1115            full.samples_interleaved[1000..1201]
1116        );
1117
1118        assert_eq!(position_to_frame(Position::Time(start), 48_000), Some(1000));
1119        assert_eq!(position_to_frame(Position::Time(stop), 48_000), Some(1201));
1120    }
1121
1122    /// `read_block` is the same read, wrapped in an interleaved audio block.
1123    #[cfg(feature = "audio-blocks")]
1124    #[test]
1125    fn test_read_block_matches_read() {
1126        let config = || ReadConfig {
1127            start: Position::Frame(1_000),
1128            stop: Position::Frame(1_500),
1129            start_channel: Some(1),
1130            num_channels: Some(2),
1131            #[cfg(feature = "resample")]
1132            sample_rate: None,
1133        };
1134
1135        let audio = read::<f32>("test_data/test_4ch.wav", config()).unwrap();
1136        let (block, sample_rate) = read_block::<f32>("test_data/test_4ch.wav", config()).unwrap();
1137
1138        assert_eq!(sample_rate, audio.sample_rate);
1139        assert_eq!(block.num_channels(), audio.num_channels);
1140        assert_eq!(block.num_frames(), 500);
1141        assert_eq!(block.raw_data(), audio.samples_interleaved.as_slice());
1142    }
1143
1144    /// A file without audio frames must report the declared channel layout and
1145    /// still validate the channel selection.
1146    #[test]
1147    fn test_read_file_without_frames() {
1148        let path = crate::tmp_path("read-empty.wav");
1149        crate::writer::write::<f32>(&path, &[], 2, 48000, crate::writer::WriteConfig::default())
1150            .unwrap();
1151
1152        let audio = read::<f32>(&path, ReadConfig::default()).unwrap();
1153        assert_eq!(audio.num_channels, 2);
1154        assert_eq!(audio.sample_rate, 48000);
1155        assert!(audio.samples_interleaved.is_empty());
1156
1157        // Resampling nothing must not fail
1158        #[cfg(feature = "resample")]
1159        {
1160            let audio = read::<f32>(
1161                &path,
1162                ReadConfig {
1163                    sample_rate: Some(24000),
1164                    ..Default::default()
1165                },
1166            )
1167            .unwrap();
1168            assert_eq!(audio.num_channels, 2);
1169            assert_eq!(audio.sample_rate, 24000);
1170            assert!(audio.samples_interleaved.is_empty());
1171        }
1172
1173        // An invalid selection must be rejected even without any audio frames
1174        match read::<f32>(
1175            &path,
1176            ReadConfig {
1177                num_channels: Some(99),
1178                ..Default::default()
1179            },
1180        ) {
1181            Err(ReadError::InvalidChannelRange { total: 2, .. }) => (),
1182            other => panic!("{other:?}"),
1183        }
1184
1185        std::fs::remove_file(&path).unwrap();
1186    }
1187}