Skip to main content

sim_lib_stream_core/packet/
pcm.rs

1//! PCM audio packet payloads: the real-time audio profile behind
2//! [`PcmPacket`](crate::PcmPacket).
3//!
4//! A PCM packet holds an interleaved block of samples described by a channel
5//! count, a frame count, and a [`PcmSampleFormat`]. Sample length must equal
6//! `channels * frames`; the constructors enforce that invariant (and finite
7//! f32 samples) so a packet is always internally consistent.
8
9use sim_kernel::{Error, Expr, Result, Symbol};
10
11use crate::buffer::symbol_field;
12
13use super::{list_field, parse_string_expr, parse_string_field};
14
15/// Sample encoding of a [`PcmPacket`].
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17pub enum PcmSampleFormat {
18    /// 16-bit signed integer samples.
19    I16,
20    /// 32-bit IEEE-754 floating-point samples.
21    F32,
22}
23
24impl PcmSampleFormat {
25    fn symbol(self) -> Symbol {
26        Symbol::qualified("pcm", self.name())
27    }
28
29    fn name(self) -> &'static str {
30        match self {
31            Self::I16 => "i16",
32            Self::F32 => "f32",
33        }
34    }
35}
36
37/// A block of interleaved PCM audio samples.
38///
39/// Carries `channels` interleaved channels of `frames` frames each in a single
40/// sample format. The total sample count always equals `channels * frames`.
41/// Equality compares f32 samples bitwise so packets with `NaN` payloads still
42/// round-trip consistently.
43#[derive(Clone, Debug)]
44pub struct PcmPacket {
45    channels: usize,
46    frames: usize,
47    samples: PcmPacketSamples,
48}
49
50impl PartialEq for PcmPacket {
51    fn eq(&self, other: &Self) -> bool {
52        self.channels == other.channels
53            && self.frames == other.frames
54            && self.samples == other.samples
55    }
56}
57
58impl Eq for PcmPacket {}
59
60#[derive(Clone, Debug)]
61enum PcmPacketSamples {
62    I16(Vec<i16>),
63    F32(Vec<f32>),
64}
65
66impl PartialEq for PcmPacketSamples {
67    fn eq(&self, other: &Self) -> bool {
68        match (self, other) {
69            (Self::I16(left), Self::I16(right)) => left == right,
70            (Self::F32(left), Self::F32(right)) => {
71                left.len() == right.len()
72                    && left
73                        .iter()
74                        .zip(right)
75                        .all(|(left, right)| left.to_bits() == right.to_bits())
76            }
77            _ => false,
78        }
79    }
80}
81
82impl Eq for PcmPacketSamples {}
83
84impl PcmPacket {
85    /// Builds an [`PcmSampleFormat::I16`] packet from interleaved samples.
86    ///
87    /// Returns an error when `channels` is zero or `samples_i16.len()` does
88    /// not equal `channels * frames`.
89    ///
90    /// # Examples
91    ///
92    /// ```
93    /// use sim_lib_stream_core::{PcmPacket, PcmSampleFormat};
94    ///
95    /// // Two channels, two frames -> four interleaved samples.
96    /// let packet = PcmPacket::i16(2, 2, vec![0, 1, 2, 3]).unwrap();
97    /// assert_eq!(packet.channels(), 2);
98    /// assert_eq!(packet.frames(), 2);
99    /// assert_eq!(packet.sample_format(), PcmSampleFormat::I16);
100    /// assert_eq!(packet.samples_i16(), &[0, 1, 2, 3]);
101    /// ```
102    pub fn i16(channels: usize, frames: usize, samples_i16: Vec<i16>) -> Result<Self> {
103        validate_pcm_shape(channels, frames, samples_i16.len())?;
104        Ok(Self {
105            channels,
106            frames,
107            samples: PcmPacketSamples::I16(samples_i16),
108        })
109    }
110
111    /// Builds an [`PcmSampleFormat::F32`] packet from interleaved samples.
112    ///
113    /// Returns an error when `channels` is zero, the sample length does not
114    /// equal `channels * frames`, or any sample is not finite.
115    pub fn f32(channels: usize, frames: usize, samples_f32: Vec<f32>) -> Result<Self> {
116        validate_pcm_shape(channels, frames, samples_f32.len())?;
117        validate_f32_samples(&samples_f32)?;
118        Ok(Self {
119            channels,
120            frames,
121            samples: PcmPacketSamples::F32(samples_f32),
122        })
123    }
124
125    /// Returns the number of interleaved channels.
126    pub fn channels(&self) -> usize {
127        self.channels
128    }
129
130    /// Returns the number of frames per channel.
131    pub fn frames(&self) -> usize {
132        self.frames
133    }
134
135    /// Returns the sample format of the stored samples.
136    pub fn sample_format(&self) -> PcmSampleFormat {
137        match self.samples {
138            PcmPacketSamples::I16(_) => PcmSampleFormat::I16,
139            PcmPacketSamples::F32(_) => PcmSampleFormat::F32,
140        }
141    }
142
143    /// Returns the interleaved i16 samples.
144    ///
145    /// # Panics
146    ///
147    /// Panics if the packet holds f32 samples; check
148    /// [`sample_format`](PcmPacket::sample_format) first.
149    pub fn samples_i16(&self) -> &[i16] {
150        match &self.samples {
151            PcmPacketSamples::I16(samples) => samples,
152            PcmPacketSamples::F32(_) => panic!("PCM packet does not contain i16 samples"),
153        }
154    }
155
156    /// Returns the interleaved f32 samples.
157    ///
158    /// # Panics
159    ///
160    /// Panics if the packet holds i16 samples; check
161    /// [`sample_format`](PcmPacket::sample_format) first.
162    pub fn samples_f32(&self) -> &[f32] {
163        match &self.samples {
164            PcmPacketSamples::F32(samples) => samples,
165            PcmPacketSamples::I16(_) => panic!("PCM packet does not contain f32 samples"),
166        }
167    }
168
169    /// Encodes the packet as a `stream/packet/pcm` [`Expr`] map.
170    pub fn to_expr(&self) -> Expr {
171        Expr::Map(vec![
172            (
173                Expr::Symbol(Symbol::new("packet")),
174                Expr::Symbol(Symbol::qualified("stream/packet", "pcm")),
175            ),
176            (
177                Expr::Symbol(Symbol::new("channels")),
178                Expr::String(self.channels.to_string()),
179            ),
180            (
181                Expr::Symbol(Symbol::new("frames")),
182                Expr::String(self.frames.to_string()),
183            ),
184            (
185                Expr::Symbol(Symbol::new("sample-format")),
186                Expr::Symbol(self.sample_format().symbol()),
187            ),
188            (
189                Expr::Symbol(Symbol::new("samples")),
190                Expr::List(self.sample_exprs()),
191            ),
192        ])
193    }
194
195    pub(super) fn from_entries(entries: &[(Expr, Expr)]) -> Result<Self> {
196        let sample_format = symbol_field(entries, "sample-format")?;
197        let channels = parse_string_field::<usize>(entries, "channels")?;
198        let frames = parse_string_field::<usize>(entries, "frames")?;
199        match sample_format.as_qualified_str().as_str() {
200            "pcm/i16" => {
201                let samples = list_field(entries, "samples")?
202                    .iter()
203                    .enumerate()
204                    .map(|(index, sample)| {
205                        parse_string_expr::<i16>(sample, "PCM i16 sample").map_err(|err| {
206                            Error::Eval(format!("invalid PCM i16 sample at {index}: {err}"))
207                        })
208                    })
209                    .collect::<Result<Vec<_>>>()?;
210                Self::i16(channels, frames, samples)
211            }
212            "pcm/f32" => {
213                let samples = list_field(entries, "samples")?
214                    .iter()
215                    .enumerate()
216                    .map(|(index, sample)| {
217                        parse_string_expr::<f32>(sample, "PCM f32 sample").map_err(|err| {
218                            Error::Eval(format!("invalid PCM f32 sample at {index}: {err}"))
219                        })
220                    })
221                    .collect::<Result<Vec<_>>>()?;
222                Self::f32(channels, frames, samples)
223            }
224            _ => Err(Error::Eval(format!(
225                "unsupported PCM sample format {}",
226                sample_format.as_qualified_str()
227            ))),
228        }
229    }
230
231    fn sample_exprs(&self) -> Vec<Expr> {
232        match &self.samples {
233            PcmPacketSamples::I16(samples) => samples
234                .iter()
235                .map(|sample| Expr::String(sample.to_string()))
236                .collect(),
237            PcmPacketSamples::F32(samples) => samples
238                .iter()
239                .map(|sample| Expr::String(sample.to_string()))
240                .collect(),
241        }
242    }
243}
244
245fn validate_pcm_shape(channels: usize, frames: usize, samples: usize) -> Result<()> {
246    if channels == 0 {
247        return Err(Error::Eval(
248            "PCM packet channel count must be greater than zero".to_owned(),
249        ));
250    }
251    let expected = channels
252        .checked_mul(frames)
253        .ok_or_else(|| Error::Eval("PCM packet sample count overflow".to_owned()))?;
254    if samples != expected {
255        return Err(Error::Eval(format!(
256            "PCM packet sample length {samples} does not match channels {channels} * frames {frames}"
257        )));
258    }
259    Ok(())
260}
261
262fn validate_f32_samples(samples: &[f32]) -> Result<()> {
263    if let Some(index) = samples.iter().position(|sample| !sample.is_finite()) {
264        return Err(Error::Eval(format!(
265            "PCM f32 sample at {index} must be finite"
266        )));
267    }
268    Ok(())
269}