Skip to main content

aria_kit/audio/
clip.rs

1use std::time::Duration;
2
3use super::{CANONICAL_SAMPLE_RATE, duration_at};
4#[cfg(not(target_family = "wasm"))]
5use super::{ResampleError, Resampler};
6
7// Telephone audio up to studio hardware. The resampler sizes its buffers by
8// the rate, so a rate read from a file header needs bounds.
9const MIN_SAMPLE_RATE: u32 = 8_000;
10const MAX_SAMPLE_RATE: u32 = 384_000;
11
12/// Mono samples and the rate they were taken at.
13#[derive(Debug, Clone, PartialEq)]
14pub struct Clip {
15    sample_rate: u32,
16    samples: Vec<f32>,
17}
18
19impl Clip {
20    pub fn new(sample_rate: u32, samples: Vec<f32>) -> Result<Self, ClipError> {
21        if !(MIN_SAMPLE_RATE..=MAX_SAMPLE_RATE).contains(&sample_rate) {
22            return Err(ClipError::SampleRate(sample_rate));
23        }
24        Ok(Self {
25            sample_rate,
26            samples,
27        })
28    }
29
30    /// Labels samples as 16 kHz. It does not resample them.
31    pub fn canonical(samples: Vec<f32>) -> Self {
32        Self {
33            sample_rate: CANONICAL_SAMPLE_RATE,
34            samples,
35        }
36    }
37
38    pub fn sample_rate(&self) -> u32 {
39        self.sample_rate
40    }
41
42    pub fn samples(&self) -> &[f32] {
43        &self.samples
44    }
45
46    pub fn into_samples(self) -> Vec<f32> {
47        self.samples
48    }
49
50    pub fn duration(&self) -> Duration {
51        duration_at(self.samples.len() as u64, self.sample_rate)
52    }
53
54    pub fn is_canonical(&self) -> bool {
55        self.sample_rate == CANONICAL_SAMPLE_RATE
56    }
57
58    #[cfg(not(target_family = "wasm"))]
59    pub fn into_canonical(self) -> Result<Self, ResampleError> {
60        if self.is_canonical() {
61            return Ok(self);
62        }
63        let samples = Resampler::convert(self.sample_rate, &self.samples)?;
64        Ok(Self::canonical(samples))
65    }
66}
67
68#[derive(Debug, thiserror::Error)]
69pub enum ClipError {
70    #[error("sample rate {0} Hz is outside 8 kHz to 384 kHz")]
71    SampleRate(u32),
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn a_clip_accepts_the_edges_of_the_rate_band() {
80        assert!(Clip::new(MIN_SAMPLE_RATE, vec![0.0]).is_ok());
81        assert!(Clip::new(MAX_SAMPLE_RATE, vec![0.0]).is_ok());
82    }
83
84    #[test]
85    fn a_clip_rejects_a_rate_outside_the_band() {
86        for rate in [0, MIN_SAMPLE_RATE - 1, MAX_SAMPLE_RATE + 1, u32::MAX] {
87            assert!(
88                matches!(Clip::new(rate, vec![0.0]), Err(ClipError::SampleRate(r)) if r == rate),
89                "{rate} Hz must be refused"
90            );
91        }
92    }
93
94    #[test]
95    fn duration_follows_the_clip_rate() {
96        let clip = Clip::new(24_000, vec![0.0; 36_000]).expect("the rate is usable");
97
98        assert_eq!(clip.duration(), Duration::from_millis(1_500));
99    }
100
101    #[cfg(not(target_family = "wasm"))]
102    #[test]
103    fn into_canonical_keeps_the_duration() {
104        let clip = Clip::new(24_000, vec![0.1; 24_000]).expect("the rate is usable");
105
106        let canonical = clip.into_canonical().expect("the clip resamples");
107
108        assert!(canonical.is_canonical());
109        assert_eq!(canonical.samples().len(), CANONICAL_SAMPLE_RATE as usize);
110    }
111}