Skip to main content

snapcast_proto/
sample_format.rs

1//! Audio sample format description.
2//!
3//! Matches the C++ `SampleFormat` class. Parses from the string format
4//! `"rate:bits:channels"` (e.g. `"48000:16:2"`). A `*` or `0` in any
5//! position means "unspecified / same as source".
6
7use std::fmt;
8use std::str::FromStr;
9
10use thiserror::Error;
11
12/// Errors when parsing a [`SampleFormat`] from a string.
13#[derive(Debug, Error)]
14pub enum SampleFormatError {
15    /// The string does not have the expected `rate:bits:channels` format.
16    #[error("sample format must be <rate>:<bits>:<channels>, got {0:?}")]
17    InvalidFormat(String),
18    /// A numeric field could not be parsed.
19    #[error("invalid number in sample format: {0}")]
20    InvalidNumber(#[from] std::num::ParseIntError),
21}
22
23/// Audio sample format: rate, bit depth, and channel count.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub struct SampleFormat {
26    rate: u32,
27    bits: u16,
28    channels: u16,
29}
30
31impl SampleFormat {
32    /// Create a new sample format.
33    pub const fn new(rate: u32, bits: u16, channels: u16) -> Self {
34        Self {
35            rate,
36            bits,
37            channels,
38        }
39    }
40
41    /// Sample rate in Hz.
42    pub fn rate(&self) -> u32 {
43        self.rate
44    }
45
46    /// Bit depth per sample.
47    pub fn bits(&self) -> u16 {
48        self.bits
49    }
50
51    /// Number of audio channels.
52    pub fn channels(&self) -> u16 {
53        self.channels
54    }
55
56    /// Size in bytes of a single mono sample.
57    ///
58    /// Note: 24-bit samples are padded to 4 bytes (matching C++ behavior).
59    pub fn sample_size(&self) -> u16 {
60        if self.bits == 24 { 4 } else { self.bits / 8 }
61    }
62
63    /// Size in bytes of one frame (all channels combined).
64    pub fn frame_size(&self) -> u16 {
65        self.channels * self.sample_size()
66    }
67
68    /// Sample rate in frames per millisecond.
69    pub fn ms_rate(&self) -> f64 {
70        f64::from(self.rate) / 1000.0
71    }
72
73    /// Convert a frame count to duration in milliseconds.
74    pub fn frames_to_ms(&self, frames: usize) -> f64 {
75        frames as f64 * 1000.0 / f64::from(self.rate)
76    }
77
78    /// Whether any field has been set (non-zero).
79    pub fn is_initialized(&self) -> bool {
80        self.rate != 0 || self.bits != 0 || self.channels != 0
81    }
82}
83
84impl FromStr for SampleFormat {
85    type Err = SampleFormatError;
86
87    /// Parse from `"rate:bits:channels"` string. `*` means 0 (unspecified).
88    fn from_str(s: &str) -> Result<Self, Self::Err> {
89        let parts: Vec<&str> = s.split(':').collect();
90        if parts.len() != 3 {
91            return Err(SampleFormatError::InvalidFormat(s.to_string()));
92        }
93        let parse = |p: &str| -> Result<u32, SampleFormatError> {
94            if p == "*" { Ok(0) } else { Ok(p.parse()?) }
95        };
96        let rate = parse(parts[0])?;
97        let bits = parse(parts[1])? as u16;
98        let channels = parse(parts[2])? as u16;
99        Ok(Self::new(rate, bits, channels))
100    }
101}
102
103impl Default for SampleFormat {
104    fn default() -> Self {
105        Self::new(0, 0, 0)
106    }
107}
108
109impl fmt::Display for SampleFormat {
110    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111        write!(f, "{}:{}:{}", self.rate, self.bits, self.channels)
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn parse_standard_format() {
121        let sf: SampleFormat = "48000:16:2".parse().unwrap();
122        assert_eq!(sf.rate(), 48000);
123        assert_eq!(sf.bits(), 16);
124        assert_eq!(sf.channels(), 2);
125    }
126
127    #[test]
128    fn parse_wildcard_channels() {
129        let sf: SampleFormat = "44100:16:*".parse().unwrap();
130        assert_eq!(sf.rate(), 44100);
131        assert_eq!(sf.bits(), 16);
132        assert_eq!(sf.channels(), 0);
133    }
134
135    #[test]
136    fn parse_all_wildcards() {
137        let sf: SampleFormat = "*:*:*".parse().unwrap();
138        assert_eq!(sf, SampleFormat::default());
139        assert!(!sf.is_initialized());
140    }
141
142    #[test]
143    fn parse_invalid_format() {
144        assert!("48000:16".parse::<SampleFormat>().is_err());
145        assert!("48000:16:2:1".parse::<SampleFormat>().is_err());
146        assert!("".parse::<SampleFormat>().is_err());
147    }
148
149    #[test]
150    fn sample_size_16bit() {
151        let sf = SampleFormat::new(48000, 16, 2);
152        assert_eq!(sf.sample_size(), 2);
153    }
154
155    #[test]
156    fn sample_size_24bit_padded_to_4() {
157        // C++ behavior: 24-bit samples are padded to 4 bytes
158        let sf = SampleFormat::new(48000, 24, 2);
159        assert_eq!(sf.sample_size(), 4);
160    }
161
162    #[test]
163    fn sample_size_32bit() {
164        let sf = SampleFormat::new(48000, 32, 2);
165        assert_eq!(sf.sample_size(), 4);
166    }
167
168    #[test]
169    fn frame_size_stereo_16bit() {
170        // 2 channels * 2 bytes = 4 bytes per frame
171        let sf = SampleFormat::new(48000, 16, 2);
172        assert_eq!(sf.frame_size(), 4);
173    }
174
175    #[test]
176    fn frame_size_stereo_24bit() {
177        // 2 channels * 4 bytes (padded) = 8 bytes per frame
178        let sf = SampleFormat::new(48000, 24, 2);
179        assert_eq!(sf.frame_size(), 8);
180    }
181
182    #[test]
183    fn ms_rate() {
184        let sf = SampleFormat::new(48000, 16, 2);
185        assert!((sf.ms_rate() - 48.0).abs() < f64::EPSILON);
186    }
187
188    #[test]
189    fn display_format() {
190        let sf = SampleFormat::new(48000, 16, 2);
191        assert_eq!(sf.to_string(), "48000:16:2");
192    }
193
194    #[test]
195    fn round_trip_string() {
196        let original = SampleFormat::new(44100, 24, 6);
197        let s = original.to_string();
198        let parsed: SampleFormat = s.parse().unwrap();
199        assert_eq!(original, parsed);
200    }
201}