1
//! POD types for the audio surface (SUPER_PLAN_2 §4 P7).
2
//!
3
//! Audio playback + microphone capture (rodio / cpal on the desktop;
4
//! AVAudioEngine / AAudio on mobile). Capture mirrors the sensor manager (the
5
//! backend pushes [`AudioFrame`]s to a process-global channel; the layout pass
6
//! drains them and a callback reads them); playback queues frames to the
7
//! backend. The mic permission is the existing
8
//! `azul_layout::managers::permission::Capability::Microphone`.
9
//!
10
//! Defined in `azul-core` so the config + frame types cross the FFI without
11
//! `azul-layout` (or rodio / cpal) as a dependency. For azul-meet (P8),
12
//! [`AudioFrame`] is the unit captured -> sent over UDP -> played back.
13

            
14
use azul_css::F32Vec;
15

            
16
/// Audio stream format.
17
#[repr(C)]
18
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19
pub struct AudioConfig {
20
    /// Samples per second per channel (e.g. 48000).
21
    pub sample_rate: u32,
22
    /// Channel count (1 = mono, 2 = stereo).
23
    pub channels: u16,
24
}
25

            
26
impl Default for AudioConfig {
27
3
    fn default() -> Self {
28
3
        Self {
29
3
            sample_rate: 48_000,
30
3
            channels: 1,
31
3
        }
32
3
    }
33
}
34

            
35
impl AudioConfig {
36
    /// A config with the given rate + channel count.
37
115
    #[must_use] pub const fn new(sample_rate: u32, channels: u16) -> Self {
38
115
        Self {
39
115
            sample_rate,
40
115
            channels,
41
115
        }
42
115
    }
43
}
44

            
45
/// A chunk of audio - interleaved `f32` samples in `[-1.0, 1.0]`.
46
///
47
/// For stereo
48
/// the layout is `L, R, L, R, ...`. This is the unit the mic backend delivers,
49
/// playback consumes, and (P8) azul-meet sends over UDP.
50
#[repr(C)]
51
#[derive(Debug, Clone, PartialEq)]
52
pub struct AudioFrame {
53
    /// Samples per second per channel.
54
    pub sample_rate: u32,
55
    /// Channel count (1 = mono, 2 = stereo).
56
    pub channels: u16,
57
    /// Interleaved `f32` samples.
58
    pub samples: F32Vec,
59
}
60

            
61
impl AudioFrame {
62
    /// Number of sample *frames* (samples per channel) in this chunk.
63
338
    #[must_use] pub fn frame_count(&self) -> usize {
64
338
        if self.channels == 0 {
65
27
            0
66
        } else {
67
311
            self.samples.as_ref().len() / self.channels as usize
68
        }
69
338
    }
70
}
71

            
72
// FFI Option wrapper for accessors that may have no frame yet. `copy = false`
73
// because AudioFrame holds a F32Vec (matches the convention in `json.rs`).
74
impl_option!(AudioFrame, OptionAudioFrame, copy = false, [Clone, Debug]);
75

            
76
#[cfg(test)]
77
mod autotest_generated {
78
    use alloc::{vec, vec::Vec};
79

            
80
    use super::*;
81

            
82
    // --- helpers ---------------------------------------------------------
83

            
84
    /// Build an `AudioFrame` from a channel count + owned sample vec.
85
    /// `AudioFrame` derives no `Default`, so each test constructs explicitly.
86
    fn frame(sample_rate: u32, channels: u16, samples: Vec<f32>) -> AudioFrame {
87
        AudioFrame {
88
            sample_rate,
89
            channels,
90
            samples: F32Vec::from_vec(samples),
91
        }
92
    }
93

            
94
    // --- AudioConfig::new (constructor) ----------------------------------
95

            
96
    /// invariants_hold: fields match the exact args, for representative input.
97
    #[test]
98
    fn config_new_fields_match_args() {
99
        let c = AudioConfig::new(48_000, 2);
100
        assert_eq!(c.sample_rate, 48_000);
101
        assert_eq!(c.channels, 2);
102
    }
103

            
104
    /// no_panic: extreme integer bounds must not panic or wrap; fields are stored verbatim.
105
    #[test]
106
    fn config_new_extreme_args_no_panic() {
107
        let zero = AudioConfig::new(0, 0);
108
        assert_eq!(zero.sample_rate, 0);
109
        assert_eq!(zero.channels, 0);
110

            
111
        let max = AudioConfig::new(u32::MAX, u16::MAX);
112
        assert_eq!(max.sample_rate, u32::MAX);
113
        assert_eq!(max.channels, u16::MAX);
114

            
115
        // A sample_rate of 1 with a very large channel count is nonsensical but legal POD.
116
        let odd = AudioConfig::new(1, u16::MAX);
117
        assert_eq!(odd.sample_rate, 1);
118
        assert_eq!(odd.channels, u16::MAX);
119
    }
120

            
121
    /// invariants_hold: `const fn` is usable in a const context (compile-time evaluation).
122
    #[test]
123
    fn config_new_is_const_evaluable() {
124
        const C: AudioConfig = AudioConfig::new(44_100, 1);
125
        assert_eq!(C.sample_rate, 44_100);
126
        assert_eq!(C.channels, 1);
127
    }
128

            
129
    /// invariants_hold: the documented default equals the equivalent explicit construction.
130
    #[test]
131
    fn config_default_matches_new() {
132
        assert_eq!(AudioConfig::default(), AudioConfig::new(48_000, 1));
133
    }
134

            
135
    /// invariants_hold: `Copy`/`Eq`/`Hash` are mutually consistent (equal values hash equal).
136
    #[test]
137
    fn config_eq_hash_consistent() {
138
        use core::hash::{Hash, Hasher};
139
        use std::collections::hash_map::DefaultHasher;
140

            
141
        let a = AudioConfig::new(96_000, 2);
142
        let b = a; // Copy
143
        assert_eq!(a, b);
144

            
145
        let mut ha = DefaultHasher::new();
146
        let mut hb = DefaultHasher::new();
147
        a.hash(&mut ha);
148
        b.hash(&mut hb);
149
        assert_eq!(ha.finish(), hb.finish());
150

            
151
        // Differing fields compare unequal.
152
        assert_ne!(AudioConfig::new(96_000, 2), AudioConfig::new(96_000, 1));
153
        assert_ne!(AudioConfig::new(96_000, 2), AudioConfig::new(48_000, 2));
154
    }
155

            
156
    // --- AudioFrame::frame_count (getter) --------------------------------
157

            
158
    /// basic_access: mono => frame_count == sample count.
159
    #[test]
160
    fn frame_count_mono_known_value() {
161
        let f = frame(48_000, 1, vec![0.0, 0.5, -0.5, 1.0]);
162
        assert_eq!(f.frame_count(), 4);
163
    }
164

            
165
    /// basic_access: stereo => frame_count == samples / 2.
166
    #[test]
167
    fn frame_count_stereo_known_value() {
168
        let f = frame(48_000, 2, vec![0.0, 0.5, -0.5, 1.0]);
169
        assert_eq!(f.frame_count(), 2);
170
    }
171

            
172
    /// edge_access: channels == 0 must hit the guard and return 0, NOT divide by zero.
173
    #[test]
174
    fn frame_count_zero_channels_no_div_by_zero() {
175
        let f = frame(48_000, 0, vec![0.0, 0.1, 0.2, 0.3, 0.4]);
176
        assert_eq!(f.frame_count(), 0);
177
    }
178

            
179
    /// edge_access: zero channels with zero samples is still 0 (guard short-circuits first).
180
    #[test]
181
    fn frame_count_zero_channels_empty_samples() {
182
        let f = frame(0, 0, Vec::new());
183
        assert_eq!(f.frame_count(), 0);
184
    }
185

            
186
    /// edge_access: empty sample buffer yields 0 frames for any nonzero channel count.
187
    #[test]
188
    fn frame_count_empty_samples() {
189
        assert_eq!(frame(48_000, 1, Vec::new()).frame_count(), 0);
190
        assert_eq!(frame(48_000, 2, Vec::new()).frame_count(), 0);
191
        assert_eq!(
192
            frame(48_000, u16::MAX, Vec::new()).frame_count(),
193
            0
194
        );
195
    }
196

            
197
    /// edge_access: a partial trailing frame is truncated by integer division (5 / 2 == 2).
198
    #[test]
199
    fn frame_count_truncates_partial_frame() {
200
        let f = frame(48_000, 2, vec![0.0, 0.1, 0.2, 0.3, 0.4]);
201
        assert_eq!(f.frame_count(), 2);
202
    }
203

            
204
    /// edge_access: channel count far exceeding the sample count yields 0, never underflow/panic.
205
    #[test]
206
    fn frame_count_channels_exceed_samples() {
207
        let f = frame(48_000, u16::MAX, vec![0.0, 1.0, -1.0]);
208
        assert_eq!(f.frame_count(), 0);
209
    }
210

            
211
    /// edge_access: exactly one full frame across the maximum channel count.
212
    #[test]
213
    fn frame_count_one_full_max_channel_frame() {
214
        let samples = vec![0.25_f32; u16::MAX as usize];
215
        let f = frame(48_000, u16::MAX, samples);
216
        assert_eq!(f.frame_count(), 1);
217
    }
218

            
219
    /// edge_access: non-finite sample values (NaN / +-inf) do not affect the length-only math.
220
    #[test]
221
    fn frame_count_ignores_non_finite_samples() {
222
        let f = frame(
223
            48_000,
224
            2,
225
            vec![f32::NAN, f32::INFINITY, f32::NEG_INFINITY, 0.0],
226
        );
227
        assert_eq!(f.frame_count(), 2);
228
    }
229

            
230
    /// edge_access: a large buffer computes without overflow and matches the plain division.
231
    #[test]
232
    fn frame_count_large_buffer() {
233
        let n = 100_000usize;
234
        let f = frame(48_000, 2, vec![0.0_f32; n]);
235
        assert_eq!(f.frame_count(), n / 2);
236
    }
237

            
238
    // --- round-trip / trait invariants -----------------------------------
239

            
240
    /// round-trip: cloning an `AudioFrame` reproduces an equal value (finite samples).
241
    #[test]
242
    fn frame_clone_round_trip_eq() {
243
        let original = frame(44_100, 2, vec![0.0, 0.5, -0.5, 1.0, -1.0, 0.25]);
244
        let cloned = original.clone();
245
        assert_eq!(original, cloned);
246
        assert_eq!(original.frame_count(), cloned.frame_count());
247
        assert_eq!(original.samples.as_ref(), cloned.samples.as_ref());
248
    }
249

            
250
    /// round-trip: `Option<AudioFrame>` <-> `OptionAudioFrame` preserves the payload both ways.
251
    #[test]
252
    fn option_audio_frame_round_trip() {
253
        let f = frame(48_000, 1, vec![0.1, 0.2, 0.3]);
254

            
255
        let wrapped: OptionAudioFrame = Some(f.clone()).into();
256
        let unwrapped: Option<AudioFrame> = wrapped.into();
257
        assert_eq!(unwrapped, Some(f));
258

            
259
        let none_wrapped: OptionAudioFrame = None.into();
260
        let none_unwrapped: Option<AudioFrame> = none_wrapped.into();
261
        assert_eq!(none_unwrapped, None);
262
    }
263

            
264
    /// numeric edge: `PartialEq` follows IEEE-754 — a NaN sample makes a frame unequal to its clone.
265
    #[test]
266
    fn frame_with_nan_is_not_self_equal() {
267
        let f = frame(48_000, 1, vec![f32::NAN]);
268
        // NaN != NaN, so PartialEq on the sample vec must report inequality.
269
        assert_ne!(f, f.clone());
270
        // ...yet the length-based getter is unaffected.
271
        assert_eq!(f.frame_count(), 1);
272
    }
273

            
274
    // --- AudioConfig: FFI / POD layout + numeric invariants --------------
275

            
276
    /// invariants_hold: `#[repr(C)]` layout is the one the FFI headers assume
277
    /// (u32 + u16 + 2 bytes tail padding, 4-byte aligned). A silent layout
278
    /// change here would corrupt every cross-language `AudioConfig`.
279
    #[test]
280
    fn config_repr_c_layout_is_stable() {
281
        assert_eq!(core::mem::size_of::<AudioConfig>(), 8);
282
        assert_eq!(core::mem::align_of::<AudioConfig>(), 4);
283
    }
284

            
285
    /// numeric: no field is truncated, sign-extended or swapped for any
286
    /// boundary combination — `new` must store both args verbatim.
287
    #[test]
288
    fn config_new_no_field_truncation_sweep() {
289
        const RATES: [u32; 8] = [
290
            0,
291
            1,
292
            8_000,
293
            44_100,
294
            48_000,
295
            192_000,
296
            u16::MAX as u32, // would alias `channels` if the fields were swapped
297
            u32::MAX,
298
        ];
299
        const CHANNELS: [u16; 6] = [0, 1, 2, 8, 255, u16::MAX];
300

            
301
        for &rate in &RATES {
302
            for &ch in &CHANNELS {
303
                let c = AudioConfig::new(rate, ch);
304
                assert_eq!(c.sample_rate, rate, "rate {rate} ch {ch}");
305
                assert_eq!(c.channels, ch, "rate {rate} ch {ch}");
306
                // Round-trip through the struct must be lossless.
307
                assert_eq!(c, AudioConfig::new(c.sample_rate, c.channels));
308
            }
309
        }
310
    }
311

            
312
    /// invariants_hold: `new` performs no normalization — a nonsense config is
313
    /// stored as given and is NOT silently coerced to the default.
314
    #[test]
315
    fn config_new_does_not_normalize() {
316
        let c = AudioConfig::new(0, 0);
317
        assert_ne!(c, AudioConfig::default());
318
        assert_eq!(c.sample_rate, 0);
319
        assert_eq!(c.channels, 0);
320
    }
321

            
322
    /// invariants_hold: `Copy` gives an independent value — mutating the copy
323
    /// must not write through to the original.
324
    #[test]
325
    fn config_copy_is_independent() {
326
        let original = AudioConfig::new(48_000, 2);
327
        let mut copy = original;
328
        copy.sample_rate = 8_000;
329
        copy.channels = 1;
330
        assert_eq!(original.sample_rate, 48_000);
331
        assert_eq!(original.channels, 2);
332
        assert_eq!(copy, AudioConfig::new(8_000, 1));
333
    }
334

            
335
    /// invariants_hold: equal configs dedup in a hash set, distinct ones do not
336
    /// (Eq/Hash agreement under a real hasher, not just `DefaultHasher`).
337
    #[test]
338
    fn config_hash_set_dedup() {
339
        use std::collections::HashSet;
340

            
341
        let mut set = HashSet::new();
342
        assert!(set.insert(AudioConfig::new(48_000, 2)));
343
        assert!(!set.insert(AudioConfig::new(48_000, 2))); // equal -> dedup
344
        assert!(set.insert(AudioConfig::new(48_000, 1))); // channels differ
345
        assert!(set.insert(AudioConfig::new(44_100, 2))); // rate differs
346
        assert!(set.insert(AudioConfig::new(u32::MAX, u16::MAX)));
347
        assert_eq!(set.len(), 4);
348
        assert!(set.contains(&AudioConfig::new(48_000, 2)));
349
    }
350

            
351
    // --- AudioFrame::frame_count: exhaustive invariant sweep --------------
352

            
353
    /// invariants_hold: for every (channels, sample-count) pair,
354
    /// `frame_count()` equals the integer division, never panics, and satisfies
355
    /// `frame_count * channels <= len` with `len - frame_count * channels < channels`.
356
    #[test]
357
    fn frame_count_division_invariant_sweep() {
358
        const CHANNELS: [u16; 9] = [0, 1, 2, 3, 5, 7, 8, 255, u16::MAX];
359

            
360
        for &ch in &CHANNELS {
361
            for len in 0..24usize {
362
                let f = frame(48_000, ch, vec![0.5_f32; len]);
363
                let fc = f.frame_count();
364

            
365
                if ch == 0 {
366
                    // Guarded: must return 0 rather than divide by zero.
367
                    assert_eq!(fc, 0, "ch=0 len={len}");
368
                    continue;
369
                }
370

            
371
                let ch_us = ch as usize;
372
                assert_eq!(fc, len / ch_us, "ch={ch} len={len}");
373
                // No over-counting: the reported frames must fit in the buffer.
374
                let consumed = fc.checked_mul(ch_us).expect("frame_count * channels overflowed");
375
                assert!(consumed <= len, "ch={ch} len={len} consumed={consumed}");
376
                // ...and at most one partial frame may be left over.
377
                assert!(len - consumed < ch_us, "ch={ch} len={len}");
378
            }
379
        }
380
    }
381

            
382
    /// edge_access: the boundary around one full max-channel frame —
383
    /// one sample short is 0 frames, one sample over is still 1 (truncation).
384
    #[test]
385
    fn frame_count_max_channel_boundaries() {
386
        let n = u16::MAX as usize;
387
        assert_eq!(frame(48_000, u16::MAX, vec![0.0; n - 1]).frame_count(), 0);
388
        assert_eq!(frame(48_000, u16::MAX, vec![0.0; n]).frame_count(), 1);
389
        assert_eq!(frame(48_000, u16::MAX, vec![0.0; n + 1]).frame_count(), 1);
390
        assert_eq!(frame(48_000, u16::MAX, vec![0.0; 2 * n]).frame_count(), 2);
391
        assert_eq!(
392
            frame(48_000, u16::MAX, vec![0.0; 2 * n - 1]).frame_count(),
393
            1
394
        );
395
    }
396

            
397
    /// invariants_hold: `sample_rate` is not an input to `frame_count` — the
398
    /// extremes must not change the result.
399
    #[test]
400
    fn frame_count_ignores_sample_rate() {
401
        let samples = vec![0.0, 0.1, 0.2, 0.3];
402
        for &rate in &[0u32, 1, 48_000, u32::MAX] {
403
            assert_eq!(frame(rate, 2, samples.clone()).frame_count(), 2, "rate={rate}");
404
        }
405
    }
406

            
407
    /// invariants_hold: `frame_count` is a pure read — repeated calls agree and
408
    /// the sample buffer is untouched.
409
    #[test]
410
    fn frame_count_is_pure() {
411
        let f = frame(48_000, 2, vec![0.0, 0.1, 0.2, 0.3, 0.4, 0.5]);
412
        let before: Vec<f32> = f.samples.as_ref().to_vec();
413
        let first = f.frame_count();
414
        let second = f.frame_count();
415
        let third = f.frame_count();
416
        assert_eq!(first, 3);
417
        assert_eq!(first, second);
418
        assert_eq!(second, third);
419
        assert_eq!(f.samples.as_ref(), before.as_slice());
420
        assert_eq!(f.samples.len(), 6);
421
    }
422

            
423
    /// edge_access: a frame whose `F32Vec` is backed by a `'static` slice
424
    /// (no heap allocation, `NoDestructor`) must count + drop cleanly.
425
    #[test]
426
    fn frame_count_static_backed_samples() {
427
        static SAMPLES: [f32; 6] = [0.0, 0.5, -0.5, 1.0, -1.0, 0.25];
428

            
429
        let f = AudioFrame {
430
            sample_rate: 48_000,
431
            channels: 2,
432
            samples: F32Vec::from_const_slice(&SAMPLES),
433
        };
434
        assert_eq!(f.frame_count(), 3);
435
        assert_eq!(f.samples.as_ref(), &SAMPLES[..]);
436

            
437
        // Cloning a static-backed vec must not alias the static into a heap free.
438
        let cloned = f.clone();
439
        assert_eq!(cloned, f);
440
        drop(f);
441
        assert_eq!(cloned.frame_count(), 3);
442
    }
443

            
444
    /// edge_access: an empty `F32Vec::new()` (null-ptr, zero-cap vec) is a valid
445
    /// zero-frame buffer and must not panic on read, clone or drop.
446
    #[test]
447
    fn frame_count_default_f32vec_is_empty() {
448
        let f = AudioFrame {
449
            sample_rate: 48_000,
450
            channels: 2,
451
            samples: F32Vec::new(),
452
        };
453
        assert_eq!(f.frame_count(), 0);
454
        assert!(f.samples.is_empty());
455
        assert_eq!(f.samples.len(), 0);
456
        assert_eq!(f.samples.as_ref(), &[] as &[f32]);
457
        assert_eq!(f.clone(), f);
458
    }
459

            
460
    /// invariants_hold: building the sample vec through `FromIterator` yields the
461
    /// same frame count as `from_vec`.
462
    #[test]
463
    fn frame_count_from_iterator_matches_from_vec() {
464
        let via_iter = AudioFrame {
465
            sample_rate: 48_000,
466
            channels: 2,
467
            samples: (0..10).map(|i| i as f32).collect::<F32Vec>(),
468
        };
469
        let via_vec = frame(48_000, 2, (0..10).map(|i| i as f32).collect::<Vec<f32>>());
470
        assert_eq!(via_iter, via_vec);
471
        assert_eq!(via_iter.frame_count(), 5);
472
        assert_eq!(via_iter.frame_count(), via_vec.frame_count());
473
    }
474

            
475
    /// edge_access: out-of-bounds sample reads return `None` rather than panicking,
476
    /// including on the last valid index of the final counted frame.
477
    #[test]
478
    fn frame_sample_access_is_bounds_checked() {
479
        let f = frame(48_000, 2, vec![0.0, 0.1, 0.2, 0.3]);
480
        let last = f.frame_count() * f.channels as usize - 1;
481
        assert_eq!(f.samples.get(last), Some(&0.3));
482
        assert_eq!(f.samples.get(f.samples.len()), None);
483
        assert_eq!(f.samples.get(usize::MAX), None);
484
    }
485

            
486
    /// numeric: extreme / non-finite / signed-zero samples survive a clone
487
    /// bit-for-bit, and none of them perturb the length-only getter.
488
    #[test]
489
    fn frame_extreme_sample_values_preserved_bitwise() {
490
        let raw = vec![
491
            f32::MIN,
492
            f32::MAX,
493
            f32::MIN_POSITIVE,
494
            -f32::MIN_POSITIVE,
495
            0.0,
496
            -0.0,
497
            f32::EPSILON,
498
            1e-45, // subnormal
499
        ];
500
        let f = frame(48_000, 2, raw.clone());
501
        assert_eq!(f.frame_count(), 4);
502

            
503
        let cloned = f.clone();
504
        let got: Vec<u32> = cloned.samples.as_ref().iter().map(|s| s.to_bits()).collect();
505
        let want: Vec<u32> = raw.iter().map(|s| s.to_bits()).collect();
506
        assert_eq!(got, want, "clone must be bit-exact (incl. -0.0 and subnormals)");
507

            
508
        // Sanity: -0.0 == 0.0 under PartialEq but their bits differ, so a
509
        // value-level compare still says equal while the bit compare above is strict.
510
        assert_eq!(cloned, f);
511
        assert_ne!(0.0_f32.to_bits(), (-0.0_f32).to_bits());
512
    }
513

            
514
    /// invariants_hold: `PartialEq` on `AudioFrame` discriminates the header
515
    /// fields, not just the samples.
516
    #[test]
517
    fn frame_eq_discriminates_header_fields() {
518
        let samples = vec![0.0, 0.5, -0.5, 1.0];
519
        let base = frame(48_000, 2, samples.clone());
520
        assert_eq!(base, frame(48_000, 2, samples.clone()));
521
        assert_ne!(base, frame(44_100, 2, samples.clone()));
522
        assert_ne!(base, frame(48_000, 1, samples.clone()));
523
        assert_ne!(base, frame(48_000, 2, vec![0.0, 0.5, -0.5]));
524
        // Same frame_count (2) via different (channels, len) pairs is still not equal.
525
        assert_eq!(frame(48_000, 1, vec![0.0, 0.5]).frame_count(), 2);
526
        assert_ne!(base, frame(48_000, 1, vec![0.0, 0.5]));
527
    }
528

            
529
    /// edge_access: repeated clone/drop cycles of a heap-backed frame must not
530
    /// double-free or corrupt the buffer (FFI vec destructor regression guard).
531
    #[test]
532
    fn frame_repeated_clone_drop_is_stable() {
533
        let original = frame(48_000, 2, (0..64).map(|i| i as f32).collect::<Vec<f32>>());
534
        for _ in 0..64 {
535
            let c = original.clone();
536
            assert_eq!(c.frame_count(), 32);
537
            assert_eq!(c.samples.as_ref(), original.samples.as_ref());
538
            drop(c);
539
        }
540
        // The original survived every clone + drop.
541
        assert_eq!(original.frame_count(), 32);
542
        assert_eq!(original.samples.len(), 64);
543
    }
544

            
545
    // --- OptionAudioFrame (FFI option wrapper) ----------------------------
546

            
547
    /// invariants_hold: the FFI option defaults to `None` and its predicates agree.
548
    #[test]
549
    fn option_audio_frame_predicates() {
550
        let none = OptionAudioFrame::default();
551
        assert!(none.is_none());
552
        assert!(!none.is_some());
553
        assert!(none.as_ref().is_none());
554
        assert!(none.as_option().is_none());
555

            
556
        let some: OptionAudioFrame = Some(frame(48_000, 2, vec![0.0, 1.0])).into();
557
        assert!(some.is_some());
558
        assert!(!some.is_none());
559
        assert_eq!(some.as_ref().map(AudioFrame::frame_count), Some(1));
560
    }
561

            
562
    /// edge_access: `into_option` clones out of a `&self`, so calling it twice must
563
    /// yield two equal, independently-owned frames (no move-out / double-free).
564
    #[test]
565
    fn option_audio_frame_into_option_twice() {
566
        let wrapped: OptionAudioFrame = Some(frame(48_000, 2, vec![0.0, 0.5, -0.5, 1.0])).into();
567
        let a = wrapped.into_option().expect("some");
568
        let b = wrapped.into_option().expect("some");
569
        assert_eq!(a, b);
570
        assert_eq!(a.frame_count(), 2);
571
        drop(a);
572
        // `b` and `wrapped` must still be intact after `a` is dropped.
573
        assert_eq!(b.frame_count(), 2);
574
        assert!(wrapped.is_some());
575
    }
576

            
577
    /// invariants_hold: `replace` returns the previous value (mem::replace semantics)
578
    /// and installs the new one.
579
    #[test]
580
    fn option_audio_frame_replace_returns_previous() {
581
        let mut slot = OptionAudioFrame::None;
582
        let prev = slot.replace(frame(48_000, 1, vec![0.0, 0.1]));
583
        assert!(prev.is_none());
584
        assert_eq!(slot.as_ref().map(AudioFrame::frame_count), Some(2));
585

            
586
        let prev = slot.replace(frame(48_000, 2, vec![0.0, 0.1, 0.2, 0.3]));
587
        assert_eq!(prev.as_ref().map(AudioFrame::frame_count), Some(2));
588
        assert_eq!(slot.as_ref().map(AudioFrame::frame_count), Some(2));
589
        assert_eq!(slot.as_ref().map(|f| f.channels), Some(2));
590
    }
591

            
592
    /// edge_access: `as_mut` hands out a live borrow — mutating the samples through it
593
    /// must be visible in the wrapper's `frame_count`.
594
    #[test]
595
    fn option_audio_frame_as_mut_mutation_visible() {
596
        let mut slot: OptionAudioFrame = Some(frame(48_000, 2, vec![0.0, 0.1, 0.2, 0.3])).into();
597
        assert_eq!(slot.as_ref().map(AudioFrame::frame_count), Some(2));
598

            
599
        if let Some(f) = slot.as_mut() {
600
            f.channels = 0; // divide-by-zero guard must engage through the wrapper too
601
        }
602
        assert_eq!(slot.as_ref().map(AudioFrame::frame_count), Some(0));
603

            
604
        if let Some(f) = slot.as_mut() {
605
            f.channels = 1;
606
        }
607
        assert_eq!(slot.as_ref().map(AudioFrame::frame_count), Some(4));
608
    }
609

            
610
    /// round-trip: an empty-sample frame survives the `Option` <-> FFI-option
611
    /// round-trip (the zero-length vec is the likeliest null-ptr edge).
612
    #[test]
613
    fn option_audio_frame_round_trip_empty_samples() {
614
        let f = frame(48_000, 2, Vec::new());
615
        let wrapped: OptionAudioFrame = Some(f.clone()).into();
616
        let back: Option<AudioFrame> = wrapped.into();
617
        let back = back.expect("some");
618
        assert_eq!(back, f);
619
        assert_eq!(back.frame_count(), 0);
620
        assert!(back.samples.is_empty());
621
    }
622
}