Skip to main content

sipx_audio/
pcm.rs

1//! Explicit linear-PCM formats and streaming rate conversion.
2
3/// Highest application PCM rate accepted by the conversion boundary.
4pub const MAX_SAMPLE_RATE: u32 = 384_000;
5
6/// A supported linear sample representation.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8#[non_exhaustive]
9pub enum PcmEncoding {
10    /// Unsigned eight-bit PCM, whose silence midpoint is 128.
11    Unsigned8,
12    /// Signed native `i16` samples.
13    Signed16,
14}
15
16/// The representation and rate of a mono linear-PCM stream.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct PcmFormat {
19    sample_rate: u32,
20    encoding: PcmEncoding,
21}
22
23impl PcmFormat {
24    /// Validate an application PCM format.
25    ///
26    /// # Errors
27    ///
28    /// Returns [`PcmError::UnsupportedSampleRate`] for zero or a rate above
29    /// [`MAX_SAMPLE_RATE`].
30    pub const fn new(sample_rate: u32, encoding: PcmEncoding) -> Result<Self, PcmError> {
31        if sample_rate == 0 || sample_rate > MAX_SAMPLE_RATE {
32            return Err(PcmError::UnsupportedSampleRate(sample_rate));
33        }
34        Ok(Self {
35            sample_rate,
36            encoding,
37        })
38    }
39
40    /// Samples per second.
41    #[must_use]
42    pub const fn sample_rate(self) -> u32 {
43        self.sample_rate
44    }
45
46    /// Sample representation.
47    #[must_use]
48    pub const fn encoding(self) -> PcmEncoding {
49        self.encoding
50    }
51}
52
53/// Owned samples whose variant states their depth.
54#[derive(Debug, Clone, PartialEq, Eq)]
55#[non_exhaustive]
56pub enum PcmSamples {
57    /// Unsigned eight-bit linear samples.
58    Unsigned8(Vec<u8>),
59    /// Signed sixteen-bit linear samples.
60    Signed16(Vec<i16>),
61}
62
63impl PcmSamples {
64    /// Number of mono samples.
65    #[must_use]
66    pub fn len(&self) -> usize {
67        match self {
68            Self::Unsigned8(samples) => samples.len(),
69            Self::Signed16(samples) => samples.len(),
70        }
71    }
72
73    /// Whether no samples are present.
74    #[must_use]
75    pub fn is_empty(&self) -> bool {
76        self.len() == 0
77    }
78}
79
80/// One owned mono PCM buffer.
81#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct Pcm {
83    format: PcmFormat,
84    samples: PcmSamples,
85}
86
87impl Pcm {
88    /// Pair a validated format with samples of the same depth.
89    ///
90    /// # Errors
91    ///
92    /// Returns [`PcmError::EncodingMismatch`] when the format and sample variant disagree.
93    pub fn new(format: PcmFormat, samples: PcmSamples) -> Result<Self, PcmError> {
94        if !matches!(
95            (format.encoding, &samples),
96            (PcmEncoding::Unsigned8, PcmSamples::Unsigned8(_))
97                | (PcmEncoding::Signed16, PcmSamples::Signed16(_))
98        ) {
99            return Err(PcmError::EncodingMismatch);
100        }
101        Ok(Self { format, samples })
102    }
103
104    /// The rate and depth attached to these samples.
105    #[must_use]
106    pub const fn format(&self) -> PcmFormat {
107        self.format
108    }
109
110    /// The owned sample representation.
111    #[must_use]
112    pub const fn samples(&self) -> &PcmSamples {
113        &self.samples
114    }
115
116    /// Consume the buffer and return its samples.
117    #[must_use]
118    pub fn into_samples(self) -> PcmSamples {
119        self.samples
120    }
121
122    /// Convert to signed 16-bit samples at `target_rate`.
123    ///
124    /// # Errors
125    ///
126    /// Returns [`PcmError::UnsupportedSampleRate`] when `target_rate` is unsupported.
127    pub fn to_i16(&self, target_rate: u32) -> Result<Vec<i16>, PcmError> {
128        let input = match &self.samples {
129            PcmSamples::Unsigned8(samples) => samples
130                .iter()
131                .map(|sample| (i16::from(*sample) - 128) << 8)
132                .collect(),
133            PcmSamples::Signed16(samples) => samples.clone(),
134        };
135        let mut resampler = LinearResampler::new(self.format.sample_rate, target_rate)?;
136        Ok(resampler.push_i16(&input))
137    }
138
139    /// Build a buffer in `format` from signed samples already at that format's rate.
140    #[must_use]
141    pub fn from_i16(format: PcmFormat, samples: Vec<i16>) -> Self {
142        let samples = match format.encoding {
143            PcmEncoding::Unsigned8 => PcmSamples::Unsigned8(
144                samples
145                    .into_iter()
146                    .map(|sample| {
147                        let shifted = (i32::from(sample) + 32_768) >> 8;
148                        u8::try_from(shifted).unwrap_or(if shifted < 0 { 0 } else { u8::MAX })
149                    })
150                    .collect(),
151            ),
152            PcmEncoding::Signed16 => PcmSamples::Signed16(samples),
153        };
154        Self { format, samples }
155    }
156}
157
158/// A typed PCM-boundary refusal.
159#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
160#[non_exhaustive]
161pub enum PcmError {
162    /// The rate is zero or beyond the supported conversion bound.
163    #[error("unsupported linear PCM sample rate {0} Hz; expected 1..={MAX_SAMPLE_RATE}")]
164    UnsupportedSampleRate(u32),
165    /// The format's depth does not match the owned sample variant.
166    #[error("linear PCM format and sample representation do not match")]
167    EncodingMismatch,
168}
169
170/// Convert one complete signed-16 stream between explicit rates.
171///
172/// For adjacent chunks use [`LinearResampler`] directly so interpolation history crosses the
173/// chunk boundary.
174///
175/// # Errors
176///
177/// Returns [`PcmError::UnsupportedSampleRate`] when either rate is unsupported.
178pub fn resample_i16(
179    samples: &[i16],
180    source_rate: u32,
181    target_rate: u32,
182) -> Result<Vec<i16>, PcmError> {
183    let mut resampler = LinearResampler::new(source_rate, target_rate)?;
184    Ok(resampler.push_i16(samples))
185}
186
187/// Streaming linear interpolation between two sample rates.
188#[derive(Debug, Clone)]
189pub struct LinearResampler {
190    source_rate: u32,
191    target_rate: u32,
192    previous: Option<i16>,
193    source_index: u64,
194    next_numerator: u64,
195}
196
197impl LinearResampler {
198    /// Start one continuous conversion stream.
199    ///
200    /// # Errors
201    ///
202    /// Returns [`PcmError::UnsupportedSampleRate`] when either rate is unsupported.
203    pub const fn new(source_rate: u32, target_rate: u32) -> Result<Self, PcmError> {
204        if source_rate == 0 || source_rate > MAX_SAMPLE_RATE {
205            return Err(PcmError::UnsupportedSampleRate(source_rate));
206        }
207        if target_rate == 0 || target_rate > MAX_SAMPLE_RATE {
208            return Err(PcmError::UnsupportedSampleRate(target_rate));
209        }
210        Ok(Self {
211            source_rate,
212            target_rate,
213            previous: None,
214            source_index: 0,
215            next_numerator: 0,
216        })
217    }
218
219    /// Convert the next adjacent signed-16 chunk.
220    #[must_use]
221    pub fn push_i16(&mut self, samples: &[i16]) -> Vec<i16> {
222        let estimate = (samples
223            .len()
224            .saturating_mul(usize::try_from(self.target_rate).unwrap_or(usize::MAX))
225            / usize::try_from(self.source_rate).unwrap_or(1))
226        .saturating_add(1);
227        let mut output = Vec::with_capacity(estimate.min(1_048_576));
228        for &current in samples {
229            let Some(previous) = self.previous else {
230                output.push(current);
231                self.previous = Some(current);
232                self.next_numerator = u64::from(self.source_rate);
233                continue;
234            };
235            self.source_index = self.source_index.saturating_add(1);
236            let interval_start = self
237                .source_index
238                .saturating_sub(1)
239                .saturating_mul(u64::from(self.target_rate));
240            let interval_end = self
241                .source_index
242                .saturating_mul(u64::from(self.target_rate));
243            while self.next_numerator <= interval_end {
244                let fraction_numerator =
245                    u32::try_from(self.next_numerator.saturating_sub(interval_start))
246                        .unwrap_or(self.target_rate);
247                let span = f64::from(self.target_rate);
248                let fraction = f64::from(fraction_numerator) / span;
249                let value =
250                    f64::from(previous) + (f64::from(current) - f64::from(previous)) * fraction;
251                output.push(round_i16(value));
252                self.next_numerator = self
253                    .next_numerator
254                    .saturating_add(u64::from(self.source_rate));
255            }
256            self.previous = Some(current);
257        }
258        output
259    }
260}
261
262#[allow(
263    clippy::cast_possible_truncation,
264    reason = "the rounded value is clamped to the i16 domain before conversion"
265)]
266fn round_i16(value: f64) -> i16 {
267    let rounded = value
268        .round()
269        .clamp(f64::from(i16::MIN), f64::from(i16::MAX));
270    i16::try_from(rounded as i32).unwrap_or(if rounded.is_sign_negative() {
271        i16::MIN
272    } else {
273        i16::MAX
274    })
275}