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