Skip to main content

nexcore_audio/
sample.rs

1// Copyright (c) 2026 Matthew Campion, PharmD; NexVigilant
2// All Rights Reserved. See LICENSE file for details.
3
4//! Audio sample types — formats, rates, and channel layouts.
5//!
6//! Tier: T2-P (N Quantity — numeric sample representation)
7//!
8//! ## Primitive Grounding
9//!
10//! | Type | Primitives | Role |
11//! |------|-----------|------|
12//! | SampleFormat | Σ + N | Numeric encoding variants |
13//! | SampleRate | N + ν | Samples per second |
14//! | ChannelLayout | Σ + N | Speaker arrangement |
15//! | AudioSpec | × (Product) | Complete format specification |
16
17use serde::{Deserialize, Serialize};
18
19/// PCM sample format (bit depth + encoding).
20///
21/// Tier: T2-P (Σ Sum — format variant union)
22#[non_exhaustive]
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
24pub enum SampleFormat {
25    /// Signed 16-bit integer (CD quality).
26    S16,
27    /// Signed 24-bit integer (packed, studio quality).
28    S24,
29    /// Signed 32-bit integer.
30    S32,
31    /// 32-bit IEEE float [-1.0, 1.0].
32    F32,
33    /// 8-bit unsigned (legacy).
34    U8,
35}
36
37impl SampleFormat {
38    /// Bytes per sample for this format.
39    pub const fn bytes_per_sample(self) -> usize {
40        match self {
41            Self::U8 => 1,
42            Self::S16 => 2,
43            Self::S24 => 3,
44            Self::S32 | Self::F32 => 4,
45        }
46    }
47
48    /// Bits per sample.
49    pub const fn bits_per_sample(self) -> u32 {
50        // bytes_per_sample() returns at most 4; cast to u32 and * 8 yields at most 32.
51        // Both the cast and the multiplication are safe for all defined SampleFormat variants.
52        #[allow(
53            clippy::as_conversions,
54            clippy::arithmetic_side_effects,
55            reason = "bytes_per_sample() <= 4 (usize); usize → u32 cast is safe here; product <= 32, fits in u32"
56        )]
57        {
58            self.bytes_per_sample() as u32 * 8
59        }
60    }
61
62    /// Whether this is a floating-point format.
63    pub const fn is_float(self) -> bool {
64        matches!(self, Self::F32)
65    }
66
67    /// Whether this is an integer format.
68    pub const fn is_integer(self) -> bool {
69        !self.is_float()
70    }
71
72    /// Human-readable name.
73    pub const fn name(self) -> &'static str {
74        match self {
75            Self::U8 => "U8",
76            Self::S16 => "S16",
77            Self::S24 => "S24",
78            Self::S32 => "S32",
79            Self::F32 => "F32",
80        }
81    }
82}
83
84impl std::fmt::Display for SampleFormat {
85    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86        write!(f, "{}", self.name())
87    }
88}
89
90/// Standard sample rates.
91///
92/// Tier: T2-P (N + ν — quantified frequency)
93#[non_exhaustive]
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
95pub enum SampleRate {
96    /// 8 kHz (telephony).
97    Hz8000,
98    /// 11.025 kHz (low quality).
99    Hz11025,
100    /// 16 kHz (wideband voice).
101    Hz16000,
102    /// 22.05 kHz (AM radio quality).
103    Hz22050,
104    /// 44.1 kHz (CD quality).
105    Hz44100,
106    /// 48 kHz (DVD/Blu-ray, default for pro audio).
107    Hz48000,
108    /// 88.2 kHz (high-res audio).
109    Hz88200,
110    /// 96 kHz (high-res audio).
111    Hz96000,
112    /// 176.4 kHz (ultra high-res).
113    Hz176400,
114    /// 192 kHz (ultra high-res).
115    Hz192000,
116    /// Custom sample rate.
117    Custom(u32),
118}
119
120impl SampleRate {
121    /// Get the rate in Hz.
122    pub const fn hz(self) -> u32 {
123        match self {
124            Self::Hz8000 => 8000,
125            Self::Hz11025 => 11025,
126            Self::Hz16000 => 16000,
127            Self::Hz22050 => 22050,
128            Self::Hz44100 => 44100,
129            Self::Hz48000 => 48000,
130            Self::Hz88200 => 88200,
131            Self::Hz96000 => 96000,
132            Self::Hz176400 => 176_400,
133            Self::Hz192000 => 192_000,
134            Self::Custom(hz) => hz,
135        }
136    }
137
138    /// Create from raw Hz value, mapping to known rates when possible.
139    pub const fn from_hz(hz: u32) -> Self {
140        match hz {
141            8000 => Self::Hz8000,
142            11025 => Self::Hz11025,
143            16000 => Self::Hz16000,
144            22050 => Self::Hz22050,
145            44100 => Self::Hz44100,
146            48000 => Self::Hz48000,
147            88200 => Self::Hz88200,
148            96000 => Self::Hz96000,
149            176_400 => Self::Hz176400,
150            192_000 => Self::Hz192000,
151            other => Self::Custom(other),
152        }
153    }
154
155    /// Whether this is a standard (non-custom) rate.
156    pub const fn is_standard(self) -> bool {
157        !matches!(self, Self::Custom(_))
158    }
159
160    /// Period in microseconds between samples.
161    pub fn period_us(self) -> f64 {
162        1_000_000.0 / f64::from(self.hz())
163    }
164}
165
166impl std::fmt::Display for SampleRate {
167    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168        let hz = self.hz();
169        if hz >= 1000 {
170            write!(f, "{}.{}kHz", hz / 1000, (hz % 1000) / 100)
171        } else {
172            write!(f, "{hz}Hz")
173        }
174    }
175}
176
177/// Channel layout — speaker arrangement.
178///
179/// Tier: T2-P (Σ + N — variant sum of channel counts)
180#[non_exhaustive]
181#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
182pub enum ChannelLayout {
183    /// Single channel.
184    Mono,
185    /// Left + Right.
186    Stereo,
187    /// 2.1 — Left, Right, LFE.
188    Surround21,
189    /// 5.1 — FL, FR, C, LFE, SL, SR.
190    Surround51,
191    /// 7.1 — FL, FR, C, LFE, SL, SR, BL, BR.
192    Surround71,
193    /// Custom channel count.
194    Custom(u16),
195}
196
197impl ChannelLayout {
198    /// Number of channels.
199    pub const fn channels(self) -> u16 {
200        match self {
201            Self::Mono => 1,
202            Self::Stereo => 2,
203            Self::Surround21 => 3,
204            Self::Surround51 => 6,
205            Self::Surround71 => 8,
206            Self::Custom(n) => n,
207        }
208    }
209
210    /// Create from channel count.
211    pub const fn from_channels(n: u16) -> Self {
212        match n {
213            1 => Self::Mono,
214            2 => Self::Stereo,
215            3 => Self::Surround21,
216            6 => Self::Surround51,
217            8 => Self::Surround71,
218            other => Self::Custom(other),
219        }
220    }
221
222    /// Whether this layout is standard.
223    pub const fn is_standard(self) -> bool {
224        !matches!(self, Self::Custom(_))
225    }
226}
227
228impl std::fmt::Display for ChannelLayout {
229    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
230        match self {
231            Self::Mono => write!(f, "Mono"),
232            Self::Stereo => write!(f, "Stereo"),
233            Self::Surround21 => write!(f, "2.1"),
234            Self::Surround51 => write!(f, "5.1"),
235            Self::Surround71 => write!(f, "7.1"),
236            Self::Custom(n) => write!(f, "{n}ch"),
237        }
238    }
239}
240
241/// Complete audio specification — format + rate + layout.
242///
243/// Tier: T2-C (× Product — composite specification)
244#[non_exhaustive]
245#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
246pub struct AudioSpec {
247    /// Sample format.
248    pub format: SampleFormat,
249    /// Sample rate.
250    pub rate: SampleRate,
251    /// Channel layout.
252    pub layout: ChannelLayout,
253}
254
255impl AudioSpec {
256    /// Create a new audio specification.
257    pub const fn new(format: SampleFormat, rate: SampleRate, layout: ChannelLayout) -> Self {
258        Self {
259            format,
260            rate,
261            layout,
262        }
263    }
264
265    /// Standard CD quality: S16 / 44.1kHz / Stereo.
266    pub const fn cd_quality() -> Self {
267        Self::new(
268            SampleFormat::S16,
269            SampleRate::Hz44100,
270            ChannelLayout::Stereo,
271        )
272    }
273
274    /// Standard DVD quality: S24 / 48kHz / 5.1.
275    pub const fn dvd_quality() -> Self {
276        Self::new(
277            SampleFormat::S24,
278            SampleRate::Hz48000,
279            ChannelLayout::Surround51,
280        )
281    }
282
283    /// Voice quality: S16 / 16kHz / Mono.
284    pub const fn voice_quality() -> Self {
285        Self::new(SampleFormat::S16, SampleRate::Hz16000, ChannelLayout::Mono)
286    }
287
288    /// Float processing: F32 / 48kHz / Stereo.
289    pub const fn float_stereo() -> Self {
290        Self::new(
291            SampleFormat::F32,
292            SampleRate::Hz48000,
293            ChannelLayout::Stereo,
294        )
295    }
296
297    /// Bytes per frame (all channels, one sample instant).
298    pub const fn bytes_per_frame(self) -> usize {
299        // channels() returns u16 (max 65535); bytes_per_sample() returns usize (max 4).
300        // Product fits in usize on any 32/64-bit platform.
301        #[allow(
302            clippy::arithmetic_side_effects,
303            clippy::as_conversions,
304            reason = "channels() is u16 cast to usize; product bounded by 4 * 65535 < usize::MAX"
305        )]
306        {
307            self.format.bytes_per_sample() * self.layout.channels() as usize
308        }
309    }
310
311    /// Bytes per second at this spec.
312    pub const fn bytes_per_second(self) -> usize {
313        // hz() is u32 (max ~4 billion); bytes_per_frame() is at most ~262140.
314        // Product bounded by ~4e9 * 262140 which can exceed u32 but fits in u64/usize on 64-bit.
315        #[allow(
316            clippy::arithmetic_side_effects,
317            clippy::as_conversions,
318            reason = "hz() is u32 cast to usize; product bounded and fits in usize on 64-bit platforms"
319        )]
320        {
321            self.bytes_per_frame() * self.rate.hz() as usize
322        }
323    }
324
325    /// Duration in seconds for a given byte count.
326    pub fn duration_secs(self, bytes: usize) -> f64 {
327        let bps = self.bytes_per_second();
328        if bps == 0 {
329            return 0.0;
330        }
331        // usize values fit exactly in f64 for all practical audio byte counts.
332        #[allow(
333            clippy::as_conversions,
334            reason = "usize → f64 cast; precision sufficient for audio duration calculations"
335        )]
336        {
337            bytes as f64 / bps as f64
338        }
339    }
340
341    /// Byte count for a given duration in seconds.
342    pub fn bytes_for_duration(self, seconds: f64) -> usize {
343        // f64 → usize: result is a byte count; negative/NaN seconds produce 0 via saturating cast.
344        #[allow(
345            clippy::as_conversions,
346            clippy::arithmetic_side_effects,
347            reason = "bytes_per_second() as f64 is exact for typical audio rates; f64 result cast to usize is bounded by valid duration inputs"
348        )]
349        {
350            (self.bytes_per_second() as f64 * seconds) as usize
351        }
352    }
353}
354
355impl std::fmt::Display for AudioSpec {
356    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
357        write!(f, "{} {} {}", self.format, self.rate, self.layout)
358    }
359}
360
361#[cfg(test)]
362mod tests {
363    use super::*;
364
365    #[test]
366    fn sample_format_bytes() {
367        assert_eq!(SampleFormat::U8.bytes_per_sample(), 1);
368        assert_eq!(SampleFormat::S16.bytes_per_sample(), 2);
369        assert_eq!(SampleFormat::S24.bytes_per_sample(), 3);
370        assert_eq!(SampleFormat::S32.bytes_per_sample(), 4);
371        assert_eq!(SampleFormat::F32.bytes_per_sample(), 4);
372    }
373
374    #[test]
375    fn sample_format_bits() {
376        assert_eq!(SampleFormat::S16.bits_per_sample(), 16);
377        assert_eq!(SampleFormat::S24.bits_per_sample(), 24);
378        assert_eq!(SampleFormat::F32.bits_per_sample(), 32);
379    }
380
381    #[test]
382    fn sample_format_float_vs_int() {
383        assert!(SampleFormat::F32.is_float());
384        assert!(!SampleFormat::S16.is_float());
385        assert!(SampleFormat::S16.is_integer());
386    }
387
388    #[test]
389    fn sample_format_display() {
390        assert_eq!(SampleFormat::S16.to_string(), "S16");
391        assert_eq!(SampleFormat::F32.to_string(), "F32");
392    }
393
394    #[test]
395    fn sample_rate_hz() {
396        assert_eq!(SampleRate::Hz44100.hz(), 44100);
397        assert_eq!(SampleRate::Hz48000.hz(), 48000);
398        assert_eq!(SampleRate::Custom(32000).hz(), 32000);
399    }
400
401    #[test]
402    fn sample_rate_from_hz() {
403        assert_eq!(SampleRate::from_hz(44100), SampleRate::Hz44100);
404        assert_eq!(SampleRate::from_hz(12345), SampleRate::Custom(12345));
405    }
406
407    #[test]
408    fn sample_rate_standard() {
409        assert!(SampleRate::Hz44100.is_standard());
410        assert!(!SampleRate::Custom(32000).is_standard());
411    }
412
413    #[test]
414    fn sample_rate_period() {
415        let period = SampleRate::Hz48000.period_us();
416        // 1_000_000 / 48000 ≈ 20.833...
417        assert!((period - 20.833).abs() < 0.1);
418    }
419
420    #[test]
421    fn sample_rate_display() {
422        assert_eq!(SampleRate::Hz44100.to_string(), "44.1kHz");
423        assert_eq!(SampleRate::Hz48000.to_string(), "48.0kHz");
424    }
425
426    #[test]
427    fn channel_layout_count() {
428        assert_eq!(ChannelLayout::Mono.channels(), 1);
429        assert_eq!(ChannelLayout::Stereo.channels(), 2);
430        assert_eq!(ChannelLayout::Surround51.channels(), 6);
431        assert_eq!(ChannelLayout::Surround71.channels(), 8);
432        assert_eq!(ChannelLayout::Custom(4).channels(), 4);
433    }
434
435    #[test]
436    fn channel_layout_from_channels() {
437        assert_eq!(ChannelLayout::from_channels(1), ChannelLayout::Mono);
438        assert_eq!(ChannelLayout::from_channels(2), ChannelLayout::Stereo);
439        assert_eq!(ChannelLayout::from_channels(4), ChannelLayout::Custom(4));
440    }
441
442    #[test]
443    fn channel_layout_display() {
444        assert_eq!(ChannelLayout::Stereo.to_string(), "Stereo");
445        assert_eq!(ChannelLayout::Surround51.to_string(), "5.1");
446        assert_eq!(ChannelLayout::Custom(4).to_string(), "4ch");
447    }
448
449    #[test]
450    fn audio_spec_cd_quality() {
451        let spec = AudioSpec::cd_quality();
452        assert_eq!(spec.format, SampleFormat::S16);
453        assert_eq!(spec.rate, SampleRate::Hz44100);
454        assert_eq!(spec.layout, ChannelLayout::Stereo);
455    }
456
457    #[test]
458    fn audio_spec_bytes_per_frame() {
459        let spec = AudioSpec::cd_quality();
460        // S16 = 2 bytes * 2 channels = 4 bytes per frame
461        assert_eq!(spec.bytes_per_frame(), 4);
462    }
463
464    #[test]
465    fn audio_spec_bytes_per_second() {
466        let spec = AudioSpec::cd_quality();
467        // 4 bytes/frame * 44100 frames/sec = 176400 bytes/sec
468        assert_eq!(spec.bytes_per_second(), 176_400);
469    }
470
471    #[test]
472    fn audio_spec_duration() {
473        let spec = AudioSpec::cd_quality();
474        let one_sec_bytes = spec.bytes_per_second();
475        let dur = spec.duration_secs(one_sec_bytes);
476        assert!((dur - 1.0).abs() < 0.001);
477    }
478
479    #[test]
480    fn audio_spec_bytes_for_duration() {
481        let spec = AudioSpec::cd_quality();
482        let bytes = spec.bytes_for_duration(1.0);
483        assert_eq!(bytes, 176_400);
484    }
485
486    #[test]
487    fn audio_spec_display() {
488        let spec = AudioSpec::cd_quality();
489        let s = spec.to_string();
490        assert!(s.contains("S16"));
491        assert!(s.contains("44.1kHz"));
492        assert!(s.contains("Stereo"));
493    }
494
495    #[test]
496    fn voice_quality_spec() {
497        let spec = AudioSpec::voice_quality();
498        assert_eq!(spec.format, SampleFormat::S16);
499        assert_eq!(spec.rate, SampleRate::Hz16000);
500        assert_eq!(spec.layout, ChannelLayout::Mono);
501        // 2 bytes/sample * 1 channel * 16000 = 32000 bytes/sec
502        assert_eq!(spec.bytes_per_second(), 32000);
503    }
504
505    #[test]
506    fn dvd_quality_spec() {
507        let spec = AudioSpec::dvd_quality();
508        assert_eq!(spec.layout, ChannelLayout::Surround51);
509        // S24 = 3 bytes * 6 channels = 18 bytes/frame * 48000 = 864000
510        assert_eq!(spec.bytes_per_second(), 864_000);
511    }
512
513    #[test]
514    fn float_stereo_spec() {
515        let spec = AudioSpec::float_stereo();
516        assert!(spec.format.is_float());
517        // F32 = 4 bytes * 2 channels = 8 bytes/frame * 48000 = 384000
518        assert_eq!(spec.bytes_per_second(), 384_000);
519    }
520}