Skip to main content

tympan_apo/
format.rs

1//! PCM audio stream format and format negotiation.
2//!
3//! [`Format`] mirrors the fields of the Windows `WAVEFORMATEX`
4//! structure plus the `WAVEFORMATEXTENSIBLE` extension (channel
5//! mask, valid-bits-per-sample, sub-format GUID). The layout is
6//! plain Rust so the type is constructible and inspectable from
7//! cross-platform code; conversions to and from the FFI structures
8//! live under `#[cfg(windows)]`.
9//!
10//! ## Extensible vs. base
11//!
12//! `WAVEFORMATEXTENSIBLE` is the canonical wire format for any
13//! stream with more than two channels, a bit depth other than 8 or
14//! 16, or an explicit channel-position mask. The base
15//! `WAVEFORMATEX` covers everything else. The framework's typed
16//! constructors (`pcm_int16`, `pcm_float32`, ...) produce the base
17//! variant; opt into the extensible variant with
18//! [`Format::with_extensible`] (which also fills in a default
19//! `channel_mask` based on the channel count).
20
21/// `WAVE_FORMAT_PCM` — integer PCM.
22pub const WAVE_FORMAT_PCM: u16 = 0x0001;
23/// `WAVE_FORMAT_IEEE_FLOAT` — IEEE 754 floating-point PCM.
24pub const WAVE_FORMAT_IEEE_FLOAT: u16 = 0x0003;
25/// `WAVE_FORMAT_EXTENSIBLE` — indicates that a
26/// `WAVEFORMATEXTENSIBLE` structure follows the base `WAVEFORMATEX`.
27pub const WAVE_FORMAT_EXTENSIBLE: u16 = 0xFFFE;
28
29/// PCM audio stream format.
30///
31/// Holds the parameters that the audio engine negotiates with an
32/// APO: format tag, channel count, sample rate, bit depth, and (for
33/// the extensible variant) channel mask plus sub-format GUID. The
34/// derived fields (`block_align`, `avg_bytes_per_sec`) are computed
35/// at construction from the inputs.
36#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
37pub struct Format {
38    /// Logical PCM type: `WAVE_FORMAT_PCM` or `WAVE_FORMAT_IEEE_FLOAT`.
39    /// The wire `wFormatTag` switches to `WAVE_FORMAT_EXTENSIBLE`
40    /// when the [`Self::extensible`] flag is set; this field
41    /// remembers the original choice so the sub-format GUID
42    /// resolves correctly.
43    format_tag: u16,
44    channels: u16,
45    samples_per_sec: u32,
46    avg_bytes_per_sec: u32,
47    block_align: u16,
48    bits_per_sample: u16,
49    /// `WAVEFORMATEXTENSIBLE::Samples::wValidBitsPerSample`. Zero
50    /// means "same as `bits_per_sample`" and only matters for the
51    /// extensible variant.
52    valid_bits_per_sample: u16,
53    /// `WAVEFORMATEXTENSIBLE::dwChannelMask`. Zero means "engine
54    /// picks the default for `channels`".
55    channel_mask: u32,
56    /// When `true`, the format crosses the COM boundary as
57    /// `WAVEFORMATEXTENSIBLE` (`wFormatTag = WAVE_FORMAT_EXTENSIBLE`,
58    /// `cbSize = 22`) and carries the channel mask + sub-format
59    /// extension. When `false`, the format uses the base
60    /// `WAVEFORMATEX` (`cbSize = 0`).
61    extensible: bool,
62}
63
64impl Format {
65    /// Construct a format directly from its fields.
66    ///
67    /// Prefer the typed constructors (`pcm_int16`, `pcm_float32`,
68    /// ...) when modelling a standard PCM stream. This raw
69    /// constructor is intended for round-tripping through
70    /// `WAVEFORMATEX` and for tests. Initialises the extension
71    /// fields (`valid_bits_per_sample`, `channel_mask`) to zero.
72    #[must_use]
73    pub const fn from_raw(
74        format_tag: u16,
75        channels: u16,
76        samples_per_sec: u32,
77        bits_per_sample: u16,
78    ) -> Self {
79        let block_align = channels * (bits_per_sample / 8);
80        let avg_bytes_per_sec = samples_per_sec * block_align as u32;
81        Self {
82            format_tag,
83            channels,
84            samples_per_sec,
85            avg_bytes_per_sec,
86            block_align,
87            bits_per_sample,
88            valid_bits_per_sample: 0,
89            channel_mask: 0,
90            extensible: false,
91        }
92    }
93
94    /// 16-bit signed integer PCM (`WAVE_FORMAT_PCM`).
95    #[must_use]
96    pub const fn pcm_int16(sample_rate: u32, channels: u16) -> Self {
97        Self::from_raw(WAVE_FORMAT_PCM, channels, sample_rate, 16)
98    }
99
100    /// 24-bit signed integer PCM packed into 3-byte containers
101    /// (`WAVE_FORMAT_PCM`).
102    #[must_use]
103    pub const fn pcm_int24(sample_rate: u32, channels: u16) -> Self {
104        Self::from_raw(WAVE_FORMAT_PCM, channels, sample_rate, 24)
105    }
106
107    /// 32-bit signed integer PCM (`WAVE_FORMAT_PCM`).
108    #[must_use]
109    pub const fn pcm_int32(sample_rate: u32, channels: u16) -> Self {
110        Self::from_raw(WAVE_FORMAT_PCM, channels, sample_rate, 32)
111    }
112
113    /// 32-bit IEEE float PCM (`WAVE_FORMAT_IEEE_FLOAT`). This is the
114    /// canonical format negotiated between the Windows audio engine
115    /// and most APOs.
116    #[must_use]
117    pub const fn pcm_float32(sample_rate: u32, channels: u16) -> Self {
118        Self::from_raw(WAVE_FORMAT_IEEE_FLOAT, channels, sample_rate, 32)
119    }
120
121    /// 64-bit IEEE float PCM (`WAVE_FORMAT_IEEE_FLOAT`).
122    #[must_use]
123    pub const fn pcm_float64(sample_rate: u32, channels: u16) -> Self {
124        Self::from_raw(WAVE_FORMAT_IEEE_FLOAT, channels, sample_rate, 64)
125    }
126
127    /// `wFormatTag` field — one of `WAVE_FORMAT_PCM`,
128    /// `WAVE_FORMAT_IEEE_FLOAT`, or `WAVE_FORMAT_EXTENSIBLE`.
129    #[inline]
130    #[must_use]
131    pub const fn format_tag(&self) -> u16 {
132        self.format_tag
133    }
134
135    /// `nChannels` field.
136    #[inline]
137    #[must_use]
138    pub const fn channels(&self) -> u16 {
139        self.channels
140    }
141
142    /// `nSamplesPerSec` field, in hertz.
143    #[inline]
144    #[must_use]
145    pub const fn sample_rate(&self) -> u32 {
146        self.samples_per_sec
147    }
148
149    /// `wBitsPerSample` field.
150    #[inline]
151    #[must_use]
152    pub const fn bits_per_sample(&self) -> u16 {
153        self.bits_per_sample
154    }
155
156    /// `nBlockAlign` field — bytes per audio frame (one sample
157    /// across all channels).
158    #[inline]
159    #[must_use]
160    pub const fn block_align(&self) -> u16 {
161        self.block_align
162    }
163
164    /// `nAvgBytesPerSec` field — `sample_rate * block_align`.
165    #[inline]
166    #[must_use]
167    pub const fn avg_bytes_per_sec(&self) -> u32 {
168        self.avg_bytes_per_sec
169    }
170
171    /// `true` if this is an IEEE-float PCM stream.
172    #[inline]
173    #[must_use]
174    pub const fn is_float(&self) -> bool {
175        self.format_tag == WAVE_FORMAT_IEEE_FLOAT
176    }
177
178    /// `true` if this is an integer PCM stream.
179    #[inline]
180    #[must_use]
181    pub const fn is_int_pcm(&self) -> bool {
182        self.format_tag == WAVE_FORMAT_PCM
183    }
184
185    /// `true` if this is the extensible variant — the wire format
186    /// uses `WAVE_FORMAT_EXTENSIBLE` and `cbSize == 22` to surface
187    /// `channel_mask` and `valid_bits_per_sample` over the wire.
188    /// The logical PCM / float distinction stays in `format_tag`
189    /// and resolves to the sub-format GUID at conversion time.
190    #[inline]
191    #[must_use]
192    pub const fn is_extensible(&self) -> bool {
193        self.extensible
194    }
195
196    /// `WAVEFORMATEXTENSIBLE::dwChannelMask` value (zero if
197    /// unspecified — the audio engine picks a default for the
198    /// channel count).
199    #[inline]
200    #[must_use]
201    pub const fn channel_mask(&self) -> u32 {
202        self.channel_mask
203    }
204
205    /// `WAVEFORMATEXTENSIBLE::Samples::wValidBitsPerSample` —
206    /// effective precision when the container is wider than the
207    /// sample (e.g. 24-bit-in-32-bit). Zero means "same as
208    /// `bits_per_sample`".
209    #[inline]
210    #[must_use]
211    pub const fn valid_bits_per_sample(&self) -> u16 {
212        self.valid_bits_per_sample
213    }
214
215    /// Promote a base `WAVEFORMATEX`-style format to the extensible
216    /// wire variant.
217    ///
218    /// Flips the `extensible` flag to `true` and fills in the
219    /// channel-position mask via [`default_channel_mask`] when
220    /// `channel_mask` is currently zero. The `format_tag` field
221    /// stays as `WAVE_FORMAT_PCM` / `WAVE_FORMAT_IEEE_FLOAT` so
222    /// the sub-format GUID can still be resolved; only the
223    /// over-the-wire `wFormatTag` changes (to
224    /// `WAVE_FORMAT_EXTENSIBLE`).
225    #[inline]
226    #[must_use]
227    pub const fn with_extensible(mut self) -> Self {
228        self.extensible = true;
229        if self.channel_mask == 0 {
230            self.channel_mask = default_channel_mask(self.channels);
231        }
232        if self.valid_bits_per_sample == 0 {
233            self.valid_bits_per_sample = self.bits_per_sample;
234        }
235        self
236    }
237
238    /// Override `channel_mask`. Useful for declaring custom
239    /// channel layouts; pass zero to clear back to the default.
240    #[inline]
241    #[must_use]
242    pub const fn with_channel_mask(mut self, mask: u32) -> Self {
243        self.channel_mask = mask;
244        self
245    }
246
247    /// Override `valid_bits_per_sample`. Pass zero to clear back
248    /// to "same as `bits_per_sample`".
249    #[inline]
250    #[must_use]
251    pub const fn with_valid_bits_per_sample(mut self, bits: u16) -> Self {
252        self.valid_bits_per_sample = bits;
253        self
254    }
255}
256
257/// Default `WAVEFORMATEXTENSIBLE::dwChannelMask` for a given channel
258/// count, matching the Microsoft "consumer convention" layouts.
259/// Returns `0` for unusual channel counts that have no canonical
260/// layout (the caller should provide an explicit mask via
261/// [`Format::with_channel_mask`]).
262#[must_use]
263pub const fn default_channel_mask(channels: u16) -> u32 {
264    // KSAUDIO_SPEAKER constants from ksmedia.h.
265    const SPEAKER_FRONT_LEFT: u32 = 0x1;
266    const SPEAKER_FRONT_RIGHT: u32 = 0x2;
267    const SPEAKER_FRONT_CENTER: u32 = 0x4;
268    const SPEAKER_LOW_FREQUENCY: u32 = 0x8;
269    const SPEAKER_BACK_LEFT: u32 = 0x10;
270    const SPEAKER_BACK_RIGHT: u32 = 0x20;
271    const SPEAKER_SIDE_LEFT: u32 = 0x200;
272    const SPEAKER_SIDE_RIGHT: u32 = 0x400;
273    match channels {
274        1 => SPEAKER_FRONT_CENTER,
275        2 => SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT,
276        // 5.1 with LFE
277        6 => {
278            SPEAKER_FRONT_LEFT
279                | SPEAKER_FRONT_RIGHT
280                | SPEAKER_FRONT_CENTER
281                | SPEAKER_LOW_FREQUENCY
282                | SPEAKER_BACK_LEFT
283                | SPEAKER_BACK_RIGHT
284        }
285        // 7.1 with LFE
286        8 => {
287            SPEAKER_FRONT_LEFT
288                | SPEAKER_FRONT_RIGHT
289                | SPEAKER_FRONT_CENTER
290                | SPEAKER_LOW_FREQUENCY
291                | SPEAKER_BACK_LEFT
292                | SPEAKER_BACK_RIGHT
293                | SPEAKER_SIDE_LEFT
294                | SPEAKER_SIDE_RIGHT
295        }
296        _ => 0,
297    }
298}
299
300#[cfg(windows)]
301impl Format {
302    /// Construct a [`Format`] from a Windows
303    /// [`WAVEFORMATEX`](windows::Win32::Media::Audio::WAVEFORMATEX).
304    ///
305    /// Only the base `WAVEFORMATEX` fields are copied; `cbSize` and
306    /// any trailing extension bytes (as used by
307    /// `WAVEFORMATEXTENSIBLE`) are ignored. To round-trip an
308    /// extensible format, deal with the extension explicitly before
309    /// or after calling this routine.
310    ///
311    /// `WAVEFORMATEX` is `#[repr(C, packed(1))]`, so this function
312    /// performs the field copies through the `{ ... }` value-context
313    /// idiom rather than taking references into the packed layout.
314    #[must_use]
315    pub fn from_waveformatex(wf: &windows::Win32::Media::Audio::WAVEFORMATEX) -> Self {
316        Self {
317            format_tag: { wf.wFormatTag },
318            channels: { wf.nChannels },
319            samples_per_sec: { wf.nSamplesPerSec },
320            avg_bytes_per_sec: { wf.nAvgBytesPerSec },
321            block_align: { wf.nBlockAlign },
322            bits_per_sample: { wf.wBitsPerSample },
323            valid_bits_per_sample: 0,
324            channel_mask: 0,
325            extensible: false,
326        }
327    }
328
329    /// Project this [`Format`] into a Windows
330    /// [`WAVEFORMATEX`](windows::Win32::Media::Audio::WAVEFORMATEX).
331    ///
332    /// `cbSize` is zero, matching plain
333    /// `WAVE_FORMAT_PCM` / `WAVE_FORMAT_IEEE_FLOAT` streams with no
334    /// trailing extension data.
335    #[must_use]
336    pub fn to_waveformatex(&self) -> windows::Win32::Media::Audio::WAVEFORMATEX {
337        windows::Win32::Media::Audio::WAVEFORMATEX {
338            wFormatTag: if self.extensible {
339                WAVE_FORMAT_EXTENSIBLE
340            } else {
341                self.format_tag
342            },
343            nChannels: self.channels,
344            nSamplesPerSec: self.samples_per_sec,
345            nAvgBytesPerSec: self.avg_bytes_per_sec,
346            nBlockAlign: self.block_align,
347            wBitsPerSample: self.bits_per_sample,
348            cbSize: if self.extensible { 22 } else { 0 },
349        }
350    }
351
352    /// Construct a [`Format`] from a Windows
353    /// `WAVEFORMATEXTENSIBLE`.
354    ///
355    /// Copies the base fields plus `dwChannelMask`,
356    /// `wValidBitsPerSample`, and resolves the sub-format GUID
357    /// back to a logical `format_tag` value (`WAVE_FORMAT_PCM` /
358    /// `WAVE_FORMAT_IEEE_FLOAT`). The wire `wFormatTag` of the
359    /// extensible struct itself is always `WAVE_FORMAT_EXTENSIBLE`;
360    /// the framework records that via the `extensible` flag.
361    #[must_use]
362    pub fn from_waveformatextensible(
363        wfx: &windows::Win32::Media::Audio::WAVEFORMATEXTENSIBLE,
364    ) -> Self {
365        let base = wfx.Format;
366        // Safety: WAVEFORMATEXTENSIBLE_0 is a `Copy` packed union;
367        // the field-level read happens through the value-context
368        // idiom so we never form a reference into the packed layout.
369        let valid_bits = unsafe { wfx.Samples.wValidBitsPerSample };
370        // Resolve the sub-format GUID back to the logical PCM /
371        // IEEE_FLOAT distinction. Anything we do not recognise
372        // falls back to PCM.
373        const KSDATAFORMAT_SUBTYPE_IEEE_FLOAT: windows_core::GUID =
374            windows_core::GUID::from_u128(0x00000003_0000_0010_8000_00aa00389b71);
375        let sub: windows_core::GUID = wfx.SubFormat;
376        let logical_tag = if sub == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT {
377            WAVE_FORMAT_IEEE_FLOAT
378        } else {
379            WAVE_FORMAT_PCM
380        };
381        Self {
382            format_tag: logical_tag,
383            channels: { base.nChannels },
384            samples_per_sec: { base.nSamplesPerSec },
385            avg_bytes_per_sec: { base.nAvgBytesPerSec },
386            block_align: { base.nBlockAlign },
387            bits_per_sample: { base.wBitsPerSample },
388            valid_bits_per_sample: valid_bits,
389            channel_mask: { wfx.dwChannelMask },
390            extensible: true,
391        }
392    }
393
394    /// Project this [`Format`] into a Windows
395    /// `WAVEFORMATEXTENSIBLE`.
396    ///
397    /// The wire `wFormatTag` is `WAVE_FORMAT_EXTENSIBLE` and the
398    /// `cbSize` is 22, regardless of the logical
399    /// [`Self::format_tag`]. The sub-format GUID is resolved from
400    /// the logical `format_tag` (PCM →
401    /// `KSDATAFORMAT_SUBTYPE_PCM`, IEEE_FLOAT →
402    /// `KSDATAFORMAT_SUBTYPE_IEEE_FLOAT`).
403    #[must_use]
404    pub fn to_waveformatextensible(&self) -> windows::Win32::Media::Audio::WAVEFORMATEXTENSIBLE {
405        use windows::Win32::Media::Audio::{WAVEFORMATEXTENSIBLE, WAVEFORMATEXTENSIBLE_0};
406        let base = windows::Win32::Media::Audio::WAVEFORMATEX {
407            // Over the wire the extensible variant always carries
408            // WAVE_FORMAT_EXTENSIBLE; the logical tag lives in
409            // the SubFormat GUID below.
410            wFormatTag: WAVE_FORMAT_EXTENSIBLE,
411            nChannels: self.channels,
412            nSamplesPerSec: self.samples_per_sec,
413            nAvgBytesPerSec: self.avg_bytes_per_sec,
414            nBlockAlign: self.block_align,
415            wBitsPerSample: self.bits_per_sample,
416            // `cbSize` for WAVEFORMATEXTENSIBLE is always 22 (the
417            // size of the extension past WAVEFORMATEX).
418            cbSize: 22,
419        };
420        let samples = WAVEFORMATEXTENSIBLE_0 {
421            wValidBitsPerSample: if self.valid_bits_per_sample == 0 {
422                self.bits_per_sample
423            } else {
424                self.valid_bits_per_sample
425            },
426        };
427        WAVEFORMATEXTENSIBLE {
428            Format: base,
429            Samples: samples,
430            dwChannelMask: self.channel_mask,
431            SubFormat: self.sub_format_guid(),
432        }
433    }
434
435    /// Sub-format GUID for the extensible variant.
436    ///
437    /// Resolves from the logical [`Self::format_tag`]:
438    /// `WAVE_FORMAT_PCM` → `KSDATAFORMAT_SUBTYPE_PCM`,
439    /// `WAVE_FORMAT_IEEE_FLOAT` → `KSDATAFORMAT_SUBTYPE_IEEE_FLOAT`.
440    /// Other values fall back to PCM.
441    fn sub_format_guid(&self) -> windows_core::GUID {
442        // `Win32_Media_KernelStreaming` exposes the PCM GUID;
443        // `KSDATAFORMAT_SUBTYPE_IEEE_FLOAT` lives in the Multimedia
444        // module which we do not enable, so we hard-code its value
445        // (matches ksmedia.h).
446        const KSDATAFORMAT_SUBTYPE_IEEE_FLOAT: windows_core::GUID =
447            windows_core::GUID::from_u128(0x00000003_0000_0010_8000_00aa00389b71);
448        if self.is_int_pcm() {
449            windows::Win32::Media::KernelStreaming::KSDATAFORMAT_SUBTYPE_PCM
450        } else {
451            KSDATAFORMAT_SUBTYPE_IEEE_FLOAT
452        }
453    }
454
455    /// Read a [`Format`] from a Windows `WAVEFORMATEX` pointer,
456    /// detecting the extensible variant via the `cbSize` /
457    /// `wFormatTag` markers and re-reading as
458    /// `WAVEFORMATEXTENSIBLE` when present.
459    ///
460    /// # Safety
461    ///
462    /// `wf` must point to a valid `WAVEFORMATEX`; if its `cbSize`
463    /// is at least 22, the bytes past the base struct must form a
464    /// valid `WAVEFORMATEXTENSIBLE` (the audio engine guarantees
465    /// this when the format tag is `WAVE_FORMAT_EXTENSIBLE`).
466    #[must_use]
467    pub unsafe fn from_waveformatex_ptr(
468        wf: *const windows::Win32::Media::Audio::WAVEFORMATEX,
469    ) -> Self {
470        // Safety: caller guarantees `wf` is a valid WAVEFORMATEX.
471        let base = unsafe { &*wf };
472        if { base.cbSize } >= 22 && { base.wFormatTag } == WAVE_FORMAT_EXTENSIBLE {
473            // Safety: when cbSize >= 22 and the tag is extensible,
474            // the audio engine guarantees the bytes past the base
475            // struct continue with the WAVEFORMATEXTENSIBLE
476            // extension.
477            let wfx = wf as *const windows::Win32::Media::Audio::WAVEFORMATEXTENSIBLE;
478            Self::from_waveformatextensible(unsafe { &*wfx })
479        } else {
480            Self::from_waveformatex(base)
481        }
482    }
483}
484
485/// Outcome of `ProcessingObject::is_input_format_supported` (and
486/// its output counterpart) — to be defined in [`crate::apo`].
487///
488/// Mirrors the three return paths defined by
489/// `IAudioProcessingObject::IsInputFormatSupported`:
490///
491/// - [`Self::Accept`] — the proposed format is acceptable as-is.
492/// - [`Self::Suggest`] — the proposed format is not acceptable, but
493///   the named alternative is.
494/// - [`Self::Reject`] — the APO cannot work with this format and
495///   has no alternative to suggest.
496#[derive(Copy, Clone, PartialEq, Eq, Debug)]
497pub enum FormatNegotiation {
498    /// Format is acceptable; the audio engine should adopt it.
499    Accept,
500    /// Format is not acceptable; suggest this alternative.
501    Suggest(Format),
502    /// Format is not acceptable and no alternative is available.
503    Reject,
504}
505
506#[cfg(test)]
507mod tests {
508    use super::*;
509
510    #[test]
511    fn pcm_float32_48k_mono_has_expected_fields() {
512        let f = Format::pcm_float32(48_000, 1);
513        assert_eq!(f.format_tag(), WAVE_FORMAT_IEEE_FLOAT);
514        assert_eq!(f.channels(), 1);
515        assert_eq!(f.sample_rate(), 48_000);
516        assert_eq!(f.bits_per_sample(), 32);
517        assert_eq!(f.block_align(), 4);
518        assert_eq!(f.avg_bytes_per_sec(), 48_000 * 4);
519        assert!(f.is_float());
520        assert!(!f.is_int_pcm());
521    }
522
523    #[test]
524    fn pcm_int16_44k1_stereo_has_expected_fields() {
525        let f = Format::pcm_int16(44_100, 2);
526        assert_eq!(f.format_tag(), WAVE_FORMAT_PCM);
527        assert_eq!(f.channels(), 2);
528        assert_eq!(f.sample_rate(), 44_100);
529        assert_eq!(f.bits_per_sample(), 16);
530        assert_eq!(f.block_align(), 4);
531        assert_eq!(f.avg_bytes_per_sec(), 44_100 * 4);
532        assert!(f.is_int_pcm());
533        assert!(!f.is_float());
534    }
535
536    #[test]
537    fn pcm_int24_48k_mono_block_align_is_3() {
538        let f = Format::pcm_int24(48_000, 1);
539        assert_eq!(f.block_align(), 3);
540        assert_eq!(f.avg_bytes_per_sec(), 48_000 * 3);
541    }
542
543    #[test]
544    fn pcm_float64_48k_5_1_block_align_is_48() {
545        let f = Format::pcm_float64(48_000, 6);
546        assert_eq!(f.block_align(), 48);
547        assert_eq!(f.avg_bytes_per_sec(), 48_000 * 48);
548    }
549
550    #[test]
551    fn negotiation_variants_distinguish() {
552        let a = FormatNegotiation::Accept;
553        let s = FormatNegotiation::Suggest(Format::pcm_float32(48_000, 1));
554        let r = FormatNegotiation::Reject;
555        assert_ne!(a, s);
556        assert_ne!(s, r);
557        assert_ne!(a, r);
558    }
559}
560
561#[cfg(all(test, windows))]
562mod windows_conv_tests {
563    use super::*;
564    use windows::Win32::Media::Audio::WAVEFORMATEX;
565
566    #[test]
567    fn windows_waveformatex_is_18_bytes_packed_one() {
568        // Sanity-check the windows crate's representation. If
569        // Microsoft ever changes WAVEFORMATEX's layout, the
570        // conversion routines need a closer look.
571        assert_eq!(core::mem::size_of::<WAVEFORMATEX>(), 18);
572        assert_eq!(core::mem::align_of::<WAVEFORMATEX>(), 1);
573    }
574
575    #[test]
576    fn pcm_float32_48k_mono_round_trips() {
577        let f = Format::pcm_float32(48_000, 1);
578        let wf = f.to_waveformatex();
579        assert_eq!({ wf.wFormatTag }, WAVE_FORMAT_IEEE_FLOAT);
580        assert_eq!({ wf.nChannels }, 1);
581        assert_eq!({ wf.nSamplesPerSec }, 48_000);
582        assert_eq!({ wf.nAvgBytesPerSec }, 48_000 * 4);
583        assert_eq!({ wf.nBlockAlign }, 4);
584        assert_eq!({ wf.wBitsPerSample }, 32);
585        assert_eq!({ wf.cbSize }, 0);
586
587        let f2 = Format::from_waveformatex(&wf);
588        assert_eq!(f, f2);
589    }
590
591    #[test]
592    fn every_typed_constructor_round_trips() {
593        for f in [
594            Format::pcm_int16(44_100, 2),
595            Format::pcm_int24(48_000, 1),
596            Format::pcm_int32(96_000, 4),
597            Format::pcm_float32(48_000, 1),
598            Format::pcm_float64(192_000, 8),
599        ] {
600            let wf = f.to_waveformatex();
601            let f2 = Format::from_waveformatex(&wf);
602            assert_eq!(f, f2, "round-trip failed for {f:?}");
603        }
604    }
605
606    #[test]
607    fn from_waveformatex_preserves_all_base_fields() {
608        let wf = WAVEFORMATEX {
609            wFormatTag: WAVE_FORMAT_PCM,
610            nChannels: 2,
611            nSamplesPerSec: 44_100,
612            nAvgBytesPerSec: 44_100 * 4,
613            nBlockAlign: 4,
614            wBitsPerSample: 16,
615            cbSize: 0,
616        };
617        let f = Format::from_waveformatex(&wf);
618        assert_eq!(f.format_tag(), WAVE_FORMAT_PCM);
619        assert_eq!(f.channels(), 2);
620        assert_eq!(f.sample_rate(), 44_100);
621        assert_eq!(f.avg_bytes_per_sec(), 44_100 * 4);
622        assert_eq!(f.block_align(), 4);
623        assert_eq!(f.bits_per_sample(), 16);
624    }
625
626    #[test]
627    fn from_waveformatex_ignores_cbsize() {
628        // The trailing extension bytes are out of scope for
629        // `Format`. We make sure a non-zero cbSize does not affect
630        // the resulting `Format`.
631        let wf = WAVEFORMATEX {
632            wFormatTag: WAVE_FORMAT_EXTENSIBLE,
633            nChannels: 6,
634            nSamplesPerSec: 48_000,
635            nAvgBytesPerSec: 48_000 * 24,
636            nBlockAlign: 24,
637            wBitsPerSample: 32,
638            cbSize: 22, // sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX)
639        };
640        let f = Format::from_waveformatex(&wf);
641        assert_eq!(f.format_tag(), WAVE_FORMAT_EXTENSIBLE);
642        assert_eq!(f.channels(), 6);
643        assert_eq!(f.sample_rate(), 48_000);
644        // `Format::to_waveformatex` zeroes cbSize again, which is
645        // the documented behaviour for the lossy round-trip.
646        assert_eq!({ f.to_waveformatex().cbSize }, 0);
647    }
648
649    #[test]
650    fn waveformatextensible_is_40_bytes_packed_one() {
651        // WAVEFORMATEX (18) + Samples union (2) + dwChannelMask (4)
652        // + SubFormat (16) = 40, all packed(1).
653        assert_eq!(
654            core::mem::size_of::<windows::Win32::Media::Audio::WAVEFORMATEXTENSIBLE>(),
655            40
656        );
657        assert_eq!(
658            core::mem::align_of::<windows::Win32::Media::Audio::WAVEFORMATEXTENSIBLE>(),
659            1
660        );
661    }
662
663    #[test]
664    fn to_waveformatextensible_sets_wire_tag_and_subformat_for_float32() {
665        let f = Format::pcm_float32(48_000, 8).with_extensible();
666        let wfx = f.to_waveformatextensible();
667        // The wire wFormatTag inside WAVEFORMATEXTENSIBLE.Format is
668        // always WAVE_FORMAT_EXTENSIBLE.
669        assert_eq!({ wfx.Format.wFormatTag }, WAVE_FORMAT_EXTENSIBLE);
670        assert_eq!({ wfx.Format.cbSize }, 22);
671        assert_eq!({ wfx.Format.nChannels }, 8);
672        // SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT.
673        const KSDATAFORMAT_SUBTYPE_IEEE_FLOAT: windows_core::GUID =
674            windows_core::GUID::from_u128(0x00000003_0000_0010_8000_00aa00389b71);
675        assert_eq!({ wfx.SubFormat }, KSDATAFORMAT_SUBTYPE_IEEE_FLOAT);
676        // wValidBitsPerSample defaults to bits_per_sample.
677        assert_eq!(unsafe { wfx.Samples.wValidBitsPerSample }, 32);
678        // dwChannelMask is the 7.1 default for 8 channels.
679        assert!({ wfx.dwChannelMask } != 0);
680    }
681
682    #[test]
683    fn to_waveformatextensible_sets_pcm_subformat_for_int16() {
684        let f = Format::pcm_int16(48_000, 2).with_extensible();
685        let wfx = f.to_waveformatextensible();
686        assert_eq!(
687            { wfx.SubFormat },
688            windows::Win32::Media::KernelStreaming::KSDATAFORMAT_SUBTYPE_PCM
689        );
690    }
691
692    #[test]
693    fn extensible_round_trips_through_waveformatextensible() {
694        let original = Format::pcm_float32(48_000, 6)
695            .with_extensible()
696            .with_valid_bits_per_sample(24);
697        let wfx = original.to_waveformatextensible();
698        let parsed = Format::from_waveformatextensible(&wfx);
699        // Logical fields preserved.
700        assert!(parsed.is_extensible());
701        assert_eq!(parsed.format_tag(), WAVE_FORMAT_IEEE_FLOAT);
702        assert_eq!(parsed.channels(), original.channels());
703        assert_eq!(parsed.sample_rate(), original.sample_rate());
704        assert_eq!(parsed.channel_mask(), original.channel_mask());
705        assert_eq!(parsed.valid_bits_per_sample(), 24);
706    }
707
708    #[test]
709    fn from_waveformatex_ptr_picks_extensible_when_cbsize_22() {
710        let f = Format::pcm_float32(48_000, 8).with_extensible();
711        let wfx = f.to_waveformatextensible();
712        // Treat &wfx as &WAVEFORMATEX — the framework's COM bridge
713        // sees only the WAVEFORMATEX prefix from GetAudioFormat.
714        let prefix: *const windows::Win32::Media::Audio::WAVEFORMATEX =
715            core::ptr::addr_of!(wfx.Format);
716        // Safety: prefix points to the WAVEFORMATEX prefix of a
717        // live WAVEFORMATEXTENSIBLE.
718        let parsed = unsafe { Format::from_waveformatex_ptr(prefix) };
719        assert!(parsed.is_extensible());
720        assert_eq!(parsed.channels(), 8);
721        assert_eq!(parsed.format_tag(), WAVE_FORMAT_IEEE_FLOAT);
722    }
723
724    #[test]
725    fn from_waveformatex_ptr_keeps_base_when_cbsize_zero() {
726        let f = Format::pcm_float32(48_000, 2);
727        let wf = f.to_waveformatex();
728        let ptr: *const windows::Win32::Media::Audio::WAVEFORMATEX = &wf;
729        // Safety: ptr points to a live WAVEFORMATEX with cbSize=0.
730        let parsed = unsafe { Format::from_waveformatex_ptr(ptr) };
731        assert!(!parsed.is_extensible());
732        assert_eq!(parsed.format_tag(), WAVE_FORMAT_IEEE_FLOAT);
733    }
734
735    #[test]
736    fn default_channel_mask_returns_known_layouts() {
737        assert_eq!(default_channel_mask(1), 0x4); // FRONT_CENTER
738        assert_eq!(default_channel_mask(2), 0x3); // FL | FR
739        assert_eq!(default_channel_mask(6), 0x3F); // 5.1
740        assert_eq!(default_channel_mask(8), 0x63F); // 7.1
741        assert_eq!(default_channel_mask(3), 0); // unusual count
742    }
743}