1use std::time::Duration;
2
3use super::{CANONICAL_SAMPLE_RATE, duration_at};
4#[cfg(not(target_family = "wasm"))]
5use super::{ResampleError, Resampler};
6
7const MIN_SAMPLE_RATE: u32 = 8_000;
10const MAX_SAMPLE_RATE: u32 = 384_000;
11
12#[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 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}