Skip to main content

tympan_aspl/
format.rs

1//! Audio stream formats.
2//!
3//! [`StreamFormat`] is a cross-platform mirror of the Core Audio
4//! `AudioStreamBasicDescription` (ASBD) — the struct that describes
5//! the sample rate, sample type, channel count, and byte layout of
6//! one stream. An AudioServerPlugin negotiates one of these with the
7//! HAL for each stream's virtual and physical format.
8//!
9//! The layout is plain Rust so the type is constructible and
10//! inspectable from any host; the conversion to and from the FFI
11//! `AudioStreamBasicDescription` will live under
12//! `cfg(target_os = "macos")` in the `raw` module once that lands.
13//!
14//! ## Canonical format
15//!
16//! The HAL's preferred interchange format — and the one the
17//! framework's IO callback assumes — is 32-bit IEEE float, packed,
18//! native-endian, interleaved linear PCM. [`StreamFormat::float32`]
19//! builds exactly that.
20
21/// `kAudioFormatLinearPCM` (`'lpcm'`) — uncompressed linear PCM.
22/// The only `format_id` an AudioServerPlugin stream uses in
23/// practice.
24pub const FORMAT_LINEAR_PCM: u32 = u32::from_be_bytes(*b"lpcm");
25
26/// The on-the-wire size of a C `AudioStreamBasicDescription`: one
27/// `f64` plus seven `u32`s plus the trailing reserved `u32` —
28/// `8 + 8 * 4 = 40` bytes. The property dispatcher reports this as
29/// the value size for the stream-format properties.
30pub const ASBD_SIZE: usize = 40;
31
32/// Linear-PCM format flags, mirroring `kAudioFormatFlag*` from
33/// `<CoreAudio/CoreAudioBaseTypes.h>`.
34pub mod flags {
35    /// `kAudioFormatFlagIsFloat` — samples are IEEE floating point
36    /// rather than integer.
37    pub const IS_FLOAT: u32 = 1 << 0;
38    /// `kAudioFormatFlagIsBigEndian` — samples are big-endian.
39    /// Absent means native/little-endian on Apple silicon and
40    /// Intel.
41    pub const IS_BIG_ENDIAN: u32 = 1 << 1;
42    /// `kAudioFormatFlagIsSignedInteger` — integer samples are
43    /// signed (ignored when [`IS_FLOAT`] is set).
44    pub const IS_SIGNED_INTEGER: u32 = 1 << 2;
45    /// `kAudioFormatFlagIsPacked` — every bit of the sample word is
46    /// significant; there is no padding.
47    pub const IS_PACKED: u32 = 1 << 3;
48    /// `kAudioFormatFlagIsAlignedHigh` — sample bits are aligned to
49    /// the high end of the sample word.
50    pub const IS_ALIGNED_HIGH: u32 = 1 << 4;
51    /// `kAudioFormatFlagIsNonInterleaved` — each channel occupies a
52    /// separate buffer rather than being interleaved.
53    pub const IS_NON_INTERLEAVED: u32 = 1 << 5;
54    /// `kAudioFormatFlagIsNonMixable` — the stream cannot be mixed
55    /// with others by the HAL.
56    pub const IS_NON_MIXABLE: u32 = 1 << 6;
57}
58
59/// The sample type and width of a linear-PCM stream.
60///
61/// Covers the sample formats an AudioServerPlugin realistically
62/// negotiates. Anything outside this set is still representable as a
63/// raw [`StreamFormat`] via [`StreamFormat::from_raw`], but the
64/// typed constructors and [`StreamFormat::sample_format`] only know
65/// these.
66#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
67#[non_exhaustive]
68pub enum SampleFormat {
69    /// 16-bit signed integer samples.
70    Int16,
71    /// 24-bit signed integer samples packed into 3-byte words.
72    Int24,
73    /// 32-bit signed integer samples.
74    Int32,
75    /// 32-bit IEEE float samples — the canonical interchange
76    /// format.
77    Float32,
78}
79
80impl SampleFormat {
81    /// Bits per individual sample (one channel of one frame).
82    #[inline]
83    #[must_use]
84    pub const fn bits_per_channel(self) -> u32 {
85        match self {
86            Self::Int16 => 16,
87            Self::Int24 => 24,
88            Self::Int32 | Self::Float32 => 32,
89        }
90    }
91
92    /// Bytes occupied by one sample in a packed stream.
93    #[inline]
94    #[must_use]
95    pub const fn bytes_per_channel(self) -> u32 {
96        match self {
97            Self::Int16 => 2,
98            Self::Int24 => 3,
99            Self::Int32 | Self::Float32 => 4,
100        }
101    }
102
103    /// The `kAudioFormatFlag*` bits this sample format implies for a
104    /// packed, interleaved, native-endian stream.
105    #[inline]
106    #[must_use]
107    pub const fn format_flags(self) -> u32 {
108        match self {
109            Self::Float32 => flags::IS_FLOAT | flags::IS_PACKED,
110            Self::Int16 | Self::Int24 | Self::Int32 => flags::IS_SIGNED_INTEGER | flags::IS_PACKED,
111        }
112    }
113}
114
115/// A cross-platform mirror of `AudioStreamBasicDescription`.
116///
117/// Constructed with the typed constructors ([`Self::float32`],
118/// [`Self::int16`], …) for the standard interleaved linear-PCM
119/// streams, or with [`Self::from_raw`] for round-tripping an ASBD
120/// the framework received over the FFI boundary. The derived
121/// per-packet / per-frame byte counts are computed at construction.
122#[derive(Copy, Clone, PartialEq, Debug)]
123pub struct StreamFormat {
124    sample_rate: f64,
125    format_id: u32,
126    format_flags: u32,
127    bytes_per_packet: u32,
128    frames_per_packet: u32,
129    bytes_per_frame: u32,
130    channels_per_frame: u32,
131    bits_per_channel: u32,
132}
133
134impl StreamFormat {
135    /// Construct a format directly from the nine ASBD fields
136    /// (`mReserved` excluded — it is always zero).
137    ///
138    /// Prefer the typed constructors for standard streams; this raw
139    /// form exists for round-tripping an `AudioStreamBasicDescription`
140    /// received from the HAL.
141    // Eight parameters, one per ASBD field the framework models; a
142    // builder would only obscure the 1:1 mapping to the C struct.
143    #[allow(clippy::too_many_arguments)]
144    #[inline]
145    #[must_use]
146    pub const fn from_raw(
147        sample_rate: f64,
148        format_id: u32,
149        format_flags: u32,
150        bytes_per_packet: u32,
151        frames_per_packet: u32,
152        bytes_per_frame: u32,
153        channels_per_frame: u32,
154        bits_per_channel: u32,
155    ) -> Self {
156        Self {
157            sample_rate,
158            format_id,
159            format_flags,
160            bytes_per_packet,
161            frames_per_packet,
162            bytes_per_frame,
163            channels_per_frame,
164            bits_per_channel,
165        }
166    }
167
168    /// Build a packed, interleaved, native-endian linear-PCM format
169    /// from a [`SampleFormat`].
170    #[must_use]
171    pub const fn linear_pcm(sample: SampleFormat, sample_rate: f64, channels: u32) -> Self {
172        let bytes_per_frame = sample.bytes_per_channel() * channels;
173        Self {
174            sample_rate,
175            format_id: FORMAT_LINEAR_PCM,
176            format_flags: sample.format_flags(),
177            // Linear PCM is one frame per packet.
178            bytes_per_packet: bytes_per_frame,
179            frames_per_packet: 1,
180            bytes_per_frame,
181            channels_per_frame: channels,
182            bits_per_channel: sample.bits_per_channel(),
183        }
184    }
185
186    /// 32-bit IEEE float interleaved linear PCM — the canonical
187    /// AudioServerPlugin interchange format.
188    #[must_use]
189    pub const fn float32(sample_rate: f64, channels: u32) -> Self {
190        Self::linear_pcm(SampleFormat::Float32, sample_rate, channels)
191    }
192
193    /// 16-bit signed integer interleaved linear PCM.
194    #[must_use]
195    pub const fn int16(sample_rate: f64, channels: u32) -> Self {
196        Self::linear_pcm(SampleFormat::Int16, sample_rate, channels)
197    }
198
199    /// 24-bit signed integer interleaved linear PCM.
200    #[must_use]
201    pub const fn int24(sample_rate: f64, channels: u32) -> Self {
202        Self::linear_pcm(SampleFormat::Int24, sample_rate, channels)
203    }
204
205    /// 32-bit signed integer interleaved linear PCM.
206    #[must_use]
207    pub const fn int32(sample_rate: f64, channels: u32) -> Self {
208        Self::linear_pcm(SampleFormat::Int32, sample_rate, channels)
209    }
210
211    /// `mSampleRate` — frames per second.
212    #[inline]
213    #[must_use]
214    pub const fn sample_rate(&self) -> f64 {
215        self.sample_rate
216    }
217
218    /// `mFormatID` — the codec / encoding identifier. Always
219    /// [`FORMAT_LINEAR_PCM`] for the typed constructors.
220    #[inline]
221    #[must_use]
222    pub const fn format_id(&self) -> u32 {
223        self.format_id
224    }
225
226    /// `mFormatFlags` — the `kAudioFormatFlag*` bit set.
227    #[inline]
228    #[must_use]
229    pub const fn format_flags(&self) -> u32 {
230        self.format_flags
231    }
232
233    /// `mBytesPerPacket`.
234    #[inline]
235    #[must_use]
236    pub const fn bytes_per_packet(&self) -> u32 {
237        self.bytes_per_packet
238    }
239
240    /// `mFramesPerPacket`.
241    #[inline]
242    #[must_use]
243    pub const fn frames_per_packet(&self) -> u32 {
244        self.frames_per_packet
245    }
246
247    /// `mBytesPerFrame` — bytes for one frame across all channels.
248    #[inline]
249    #[must_use]
250    pub const fn bytes_per_frame(&self) -> u32 {
251        self.bytes_per_frame
252    }
253
254    /// `mChannelsPerFrame` — the channel count.
255    #[inline]
256    #[must_use]
257    pub const fn channels(&self) -> u32 {
258        self.channels_per_frame
259    }
260
261    /// `mBitsPerChannel` — significant bits per sample.
262    #[inline]
263    #[must_use]
264    pub const fn bits_per_channel(&self) -> u32 {
265        self.bits_per_channel
266    }
267
268    /// `true` iff this is a linear-PCM stream
269    /// ([`FORMAT_LINEAR_PCM`]).
270    #[inline]
271    #[must_use]
272    pub const fn is_linear_pcm(&self) -> bool {
273        self.format_id == FORMAT_LINEAR_PCM
274    }
275
276    /// `true` iff the samples are IEEE floating point.
277    #[inline]
278    #[must_use]
279    pub const fn is_float(&self) -> bool {
280        self.format_flags & flags::IS_FLOAT != 0
281    }
282
283    /// `true` iff every bit of the sample word is significant.
284    #[inline]
285    #[must_use]
286    pub const fn is_packed(&self) -> bool {
287        self.format_flags & flags::IS_PACKED != 0
288    }
289
290    /// `true` iff each channel occupies its own buffer.
291    #[inline]
292    #[must_use]
293    pub const fn is_non_interleaved(&self) -> bool {
294        self.format_flags & flags::IS_NON_INTERLEAVED != 0
295    }
296
297    /// Identify the [`SampleFormat`] of this stream, or `None` if it
298    /// is not one of the four standard packed interleaved layouts.
299    #[must_use]
300    pub fn sample_format(&self) -> Option<SampleFormat> {
301        if !self.is_linear_pcm() || !self.is_packed() {
302            return None;
303        }
304        [
305            SampleFormat::Int16,
306            SampleFormat::Int24,
307            SampleFormat::Int32,
308            SampleFormat::Float32,
309        ]
310        .into_iter()
311        .find(|candidate| {
312            candidate.bits_per_channel() == self.bits_per_channel
313                && candidate.format_flags() == self.format_flags
314        })
315    }
316
317    /// `true` iff this is the canonical interchange format: packed,
318    /// interleaved 32-bit float linear PCM.
319    #[inline]
320    #[must_use]
321    pub fn is_canonical(&self) -> bool {
322        self.sample_format() == Some(SampleFormat::Float32) && !self.is_non_interleaved()
323    }
324}
325
326/// The outcome of a stream-format negotiation.
327///
328/// Mirrors the three ways an AudioServerPlugin can answer the HAL's
329/// "can you do this format?" query
330/// (`AudioServerPlugInDriverInterface::GetPropertyData` over
331/// `kAudioStreamPropertyAvailableVirtualFormats` and the format
332/// setters):
333///
334/// - [`Self::Accept`] — the proposed format is supported as-is.
335/// - [`Self::Suggest`] — the proposed format is not supported, but
336///   the named alternative is the closest one that is.
337/// - [`Self::Reject`] — the format is not supported and there is no
338///   sensible alternative to offer.
339#[derive(Copy, Clone, PartialEq, Debug)]
340pub enum FormatNegotiation {
341    /// The proposed format is supported; adopt it unchanged.
342    Accept,
343    /// The proposed format is not supported; this alternative is.
344    Suggest(StreamFormat),
345    /// The proposed format is not supported and no alternative is
346    /// offered.
347    Reject,
348}
349
350#[cfg(test)]
351mod tests {
352    use super::*;
353
354    #[test]
355    fn float32_48k_stereo_has_expected_fields() {
356        let f = StreamFormat::float32(48_000.0, 2);
357        assert_eq!(f.sample_rate(), 48_000.0);
358        assert_eq!(f.format_id(), FORMAT_LINEAR_PCM);
359        assert!(f.is_linear_pcm());
360        assert!(f.is_float());
361        assert!(f.is_packed());
362        assert!(!f.is_non_interleaved());
363        assert_eq!(f.channels(), 2);
364        assert_eq!(f.bits_per_channel(), 32);
365        assert_eq!(f.bytes_per_frame(), 8);
366        assert_eq!(f.bytes_per_packet(), 8);
367        assert_eq!(f.frames_per_packet(), 1);
368    }
369
370    #[test]
371    fn int16_44k1_mono_has_expected_fields() {
372        let f = StreamFormat::int16(44_100.0, 1);
373        assert!(!f.is_float());
374        assert_eq!(f.bits_per_channel(), 16);
375        assert_eq!(f.bytes_per_frame(), 2);
376    }
377
378    #[test]
379    fn int24_packs_into_three_bytes() {
380        let f = StreamFormat::int24(48_000.0, 2);
381        assert_eq!(f.bits_per_channel(), 24);
382        assert_eq!(f.bytes_per_frame(), 6);
383    }
384
385    #[test]
386    fn int32_uses_four_byte_words() {
387        let f = StreamFormat::int32(96_000.0, 4);
388        assert_eq!(f.bits_per_channel(), 32);
389        assert_eq!(f.bytes_per_frame(), 16);
390    }
391
392    #[test]
393    fn sample_format_round_trips_through_constructors() {
394        let cases = [
395            (SampleFormat::Int16, StreamFormat::int16(48_000.0, 2)),
396            (SampleFormat::Int24, StreamFormat::int24(48_000.0, 2)),
397            (SampleFormat::Int32, StreamFormat::int32(48_000.0, 2)),
398            (SampleFormat::Float32, StreamFormat::float32(48_000.0, 2)),
399        ];
400        for (sample, format) in cases {
401            assert_eq!(format.sample_format(), Some(sample));
402        }
403    }
404
405    #[test]
406    fn only_float32_interleaved_is_canonical() {
407        assert!(StreamFormat::float32(48_000.0, 2).is_canonical());
408        assert!(!StreamFormat::int16(48_000.0, 2).is_canonical());
409        assert!(!StreamFormat::int32(48_000.0, 2).is_canonical());
410    }
411
412    #[test]
413    fn non_interleaved_float32_is_not_canonical() {
414        let mut raw = StreamFormat::float32(48_000.0, 2);
415        raw = StreamFormat::from_raw(
416            raw.sample_rate(),
417            raw.format_id(),
418            raw.format_flags() | flags::IS_NON_INTERLEAVED,
419            raw.bytes_per_packet(),
420            raw.frames_per_packet(),
421            raw.bytes_per_frame(),
422            raw.channels(),
423            raw.bits_per_channel(),
424        );
425        assert!(raw.is_non_interleaved());
426        assert!(!raw.is_canonical());
427    }
428
429    #[test]
430    fn unknown_format_id_has_no_sample_format() {
431        let raw =
432            StreamFormat::from_raw(48_000.0, u32::from_be_bytes(*b"aac "), 0, 0, 1024, 0, 2, 0);
433        assert!(!raw.is_linear_pcm());
434        assert_eq!(raw.sample_format(), None);
435        assert!(!raw.is_canonical());
436    }
437
438    #[test]
439    fn format_linear_pcm_constant_is_lpcm() {
440        assert_eq!(FORMAT_LINEAR_PCM, 0x6C70_636D);
441    }
442
443    #[test]
444    fn sample_format_flag_sets_are_distinct() {
445        assert_ne!(
446            SampleFormat::Float32.format_flags(),
447            SampleFormat::Int32.format_flags()
448        );
449        assert!(SampleFormat::Float32.format_flags() & flags::IS_FLOAT != 0);
450        assert!(SampleFormat::Int16.format_flags() & flags::IS_SIGNED_INTEGER != 0);
451    }
452
453    #[test]
454    fn negotiation_variants_distinguish() {
455        let accept = FormatNegotiation::Accept;
456        let suggest = FormatNegotiation::Suggest(StreamFormat::float32(48_000.0, 2));
457        let reject = FormatNegotiation::Reject;
458        assert_eq!(accept, FormatNegotiation::Accept);
459        assert_ne!(accept, reject);
460        assert_ne!(suggest, accept);
461        assert_ne!(suggest, reject);
462    }
463}