1use crate::{AudioLoss, Error, Event, Result};
3use std::borrow::Cow;
4use std::num::NonZeroU64;
5use std::time::Duration;
6
7pub fn discarded_startup_loss(
10 samples: &[f32],
11 rate: u32,
12 channels: u16,
13) -> Result<Option<AudioLoss>> {
14 if rate == 0 || channels == 0 || samples.len() % usize::from(channels) != 0 {
15 return Err(Error::UnsupportedFormat("startup loss requires a nonzero native rate and whole interleaved frames; recreate the stream with a valid input format".into()));
16 }
17 let count = u64::try_from(samples.len()).map_err(|_| {
18 Error::InvalidState("startup loss count exhausted; reopen the capture stream".into())
19 })?;
20 NonZeroU64::new(count)
21 .map(|count| AudioLoss::generation_ended(Some(count), rate, channels))
22 .transpose()
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum StartupDropReason {
28 OutOfRange,
30}
31impl StartupDropReason {
32 pub fn as_str(self) -> &'static str {
34 "outOfRange"
35 }
36}
37pub struct StartupSamples {
40 rate: u32,
41 channels: u16,
42 finished: bool,
43 seen: u64,
44 dropped: u64,
45 pending: Vec<f32>,
46 confirmation_frames: usize,
47}
48pub struct StartupOutput<'a> {
50 pub samples: Cow<'a, [f32]>,
52 pub first_frame: u64,
54 pub event: Option<Event>,
56}
57impl StartupSamples {
58 pub fn new(rate: u32, channels: u16) -> Self {
60 Self {
61 rate,
62 channels,
63 finished: false,
64 seen: 0,
65 dropped: 0,
66 pending: Vec::new(),
67 confirmation_frames: (u64::from(rate).div_ceil(100)) as usize,
68 }
69 }
70 pub fn is_pending(&self) -> bool {
72 !self.finished
73 }
74 pub fn dropped_frames(&self) -> u64 {
76 self.dropped
77 }
78 pub fn dropped_event(&self) -> Option<Event> {
80 (self.dropped > 0).then_some(Event::StartupSamplesDropped {
81 frames: self.dropped,
82 reason: StartupDropReason::OutOfRange,
83 })
84 }
85 pub fn check_deadline(&self, elapsed: Duration) -> Result<()> {
87 if !self.finished && self.dropped > 0 && elapsed >= Duration::from_secs(2) {
88 return Err(Self::timeout());
89 }
90 Ok(())
91 }
92 fn timeout() -> Error {
93 Error::Backend("microphone input keeps returning out-of-range or non-finite startup samples; valid input was not confirmed within 2 seconds. Reconnect the input, check its driver and recording settings, then reopen the stream".into())
94 }
95 pub fn finish(&mut self) -> StartupOutput<'static> {
98 if self.finished {
99 return StartupOutput {
100 samples: Cow::Owned(Vec::new()),
101 first_frame: self.seen,
102 event: None,
103 };
104 }
105 self.finished = true;
106 if self.dropped == 0 {
107 return StartupOutput {
108 samples: Cow::Owned(std::mem::take(&mut self.pending)),
109 first_frame: 0,
110 event: None,
111 };
112 }
113 self.dropped = self
114 .dropped
115 .saturating_add((self.pending.len() / usize::from(self.channels.max(1))) as u64);
116 self.pending.clear();
117 StartupOutput {
118 samples: Cow::Owned(Vec::new()),
119 first_frame: self.seen,
120 event: self.dropped_event(),
121 }
122 }
123 pub fn push<'a>(&mut self, samples: &'a [f32], elapsed: Duration) -> Result<StartupOutput<'a>> {
125 let channels = usize::from(self.channels);
126 if self.rate == 0 || channels == 0 || samples.len() % channels != 0 {
127 return Err(Error::UnsupportedFormat("microphone startup validation requires a nonzero native rate and whole interleaved frames; recreate the stream with a valid input format".into()));
128 }
129 self.check_deadline(elapsed)?;
130 let first_frame = self.seen;
131 if self.finished {
132 self.seen = self.seen.saturating_add((samples.len() / channels) as u64);
133 return Ok(StartupOutput {
134 samples: Cow::Borrowed(samples),
135 first_frame,
136 event: None,
137 });
138 }
139 for (index, frame) in samples.chunks_exact(channels).enumerate() {
140 self.seen = self.seen.saturating_add(1);
141 let valid = frame
142 .iter()
143 .all(|sample| sample.is_finite() && sample.abs() <= 1.0);
144 if !valid {
145 self.dropped = self
146 .dropped
147 .saturating_add(1 + (self.pending.len() / channels) as u64);
148 self.pending.clear();
149 } else {
150 self.pending.extend_from_slice(frame);
151 if self.pending.len() / channels >= self.confirmation_frames {
152 self.finished = true;
153 self.pending
154 .extend_from_slice(&samples[(index + 1) * channels..]);
155 self.seen = first_frame.saturating_add((samples.len() / channels) as u64);
156 return Ok(StartupOutput {
157 samples: Cow::Owned(std::mem::take(&mut self.pending)),
158 first_frame: self.dropped,
159 event: self.dropped_event(),
160 });
161 }
162 }
163 if self.seen >= u64::from(self.rate) * 2 {
164 return Err(Self::timeout());
165 }
166 }
167 Ok(StartupOutput {
168 samples: Cow::Borrowed(&[]),
169 first_frame: self.seen,
170 event: None,
171 })
172 }
173}
174#[cfg(test)]
175mod tests {
176 use super::*;
177 #[test]
178 fn startup_invalid_prefix_is_dropped_and_confirmation_is_replayed() -> Result<()> {
179 let mut guard = StartupSamples::new(48_000, 1);
180 assert!(guard
181 .push(&[3.058; 480], Duration::ZERO)?
182 .samples
183 .is_empty());
184 assert!(guard
185 .push(&[0.0; 240], Duration::from_millis(10))?
186 .samples
187 .is_empty());
188 let result = guard.push(&[0.25; 240], Duration::from_millis(15))?;
189 assert_eq!(result.first_frame, 480);
190 assert_eq!(&result.samples[..240], &[0.0; 240]);
191 assert_eq!(&result.samples[240..], &[0.25; 240]);
192 assert!(result.event.is_some());
193 assert!(guard
194 .push(&[3.0], Duration::from_millis(20))?
195 .event
196 .is_none());
197 Ok(())
198 }
199 #[test]
200 fn startup_valid_zero_prefix_is_unchanged_without_drops() -> Result<()> {
201 let mut guard = StartupSamples::new(48_000, 2);
202 let samples: Vec<f32> = [0.0, -0.0, 0.25, -0.25].repeat(240);
203 let result = guard.push(&samples, Duration::ZERO)?;
204 assert_eq!(&*result.samples, &samples);
205 assert_eq!(result.first_frame, 0);
206 assert!(result.event.is_none());
207 Ok(())
208 }
209 #[test]
210 fn startup_invalid_input_fails_at_two_seconds() -> Result<()> {
211 let mut guard = StartupSamples::new(48_000, 1);
212 let _ = guard.push(&[f32::NAN; 480], Duration::ZERO)?;
213 let result = guard.push(&[3.0; 480], Duration::from_secs(2));
214 assert!(
215 result.is_err(),
216 "invalid input must fail rather than be delivered"
217 );
218 Ok(())
219 }
220}
221
222#[cfg(test)]
223mod boundary_tests {
224 use super::*;
225 #[test]
226 fn interrupted_confirmation_discards_whole_stereo_frames_and_reports_once() -> Result<()> {
227 let mut guard = StartupSamples::new(44_100, 2);
228 let _ = guard.push(&[0.5, f32::INFINITY], Duration::ZERO)?;
229 let _ = guard.push(&[0.0; 440], Duration::from_millis(1))?;
230 let _ = guard.push(&[f32::NAN, 0.5], Duration::from_millis(5))?;
231 let valid = vec![0.25; 882];
232 let result = guard.push(&valid, Duration::from_millis(6))?;
233 assert_eq!(result.first_frame, 222);
234 assert_eq!(&*result.samples, &valid);
235 assert_eq!(
236 result.event,
237 Some(Event::StartupSamplesDropped {
238 frames: 222,
239 reason: StartupDropReason::OutOfRange
240 })
241 );
242 let next = guard.push(&[2.0, f32::NAN], Duration::from_secs(3))?;
243 assert_eq!(next.samples[0], 2.0);
244 assert!(next.samples[1].is_nan());
245 assert!(next.event.is_none());
246 Ok(())
247 }
248 #[test]
249 fn confirmed_window_disables_filter_for_midstream_out_of_range() -> Result<()> {
250 let mut guard = StartupSamples::new(48_000, 1);
251 let _ = guard.push(&[0.0; 480], Duration::ZERO)?;
252 let samples = [0.0, 3.058, f32::NAN];
253 let output = guard.push(&samples, Duration::ZERO)?;
254 assert_eq!(output.samples[0], 0.0);
255 assert_eq!(output.samples[1], 3.058);
256 assert!(output.samples[2].is_nan());
257 assert!(output.event.is_none());
258 assert_eq!(guard.dropped_frames(), 0);
259 Ok(())
260 }
261 #[test]
262 fn invalid_input_deadline_does_not_require_another_callback() -> Result<()> {
263 let mut guard = StartupSamples::new(48_000, 1);
264 let _ = guard.push(&[2.0], Duration::ZERO)?;
265 assert!(guard.check_deadline(Duration::from_millis(1999)).is_ok());
266 let error = guard
267 .check_deadline(Duration::from_secs(2))
268 .expect_err("deadline must fail");
269 assert!(error.to_string().contains("out-of-range"));
270 assert!(error.to_string().contains("reopen"));
271 Ok(())
272 }
273}
274
275#[cfg(test)]
276mod leading_window_tests {
277 use super::*;
278 #[test]
279 fn valid_first_sample_does_not_release_an_invalid_startup_window() -> Result<()> {
280 let mut guard = StartupSamples::new(48_000, 1);
281 let mut samples = vec![0.0, 3.058];
282 samples.extend([0.25; 480]);
283 let output = guard.push(&samples, Duration::ZERO)?;
284 assert_eq!(&*output.samples, &[0.25; 480]);
285 assert_eq!(output.first_frame, 2);
286 assert_eq!(
287 output.event,
288 Some(Event::StartupSamplesDropped {
289 frames: 2,
290 reason: StartupDropReason::OutOfRange
291 })
292 );
293 Ok(())
294 }
295}
296
297#[cfg(test)]
298mod finish_tests {
299 use super::*;
300 #[test]
301 fn short_valid_zero_capture_is_delivered_on_finish_without_a_drop() -> Result<()> {
302 let mut guard = StartupSamples::new(48_000, 2);
303 let zeros = [0.0; 26];
304 assert!(guard.push(&zeros, Duration::ZERO)?.samples.is_empty());
305 let tail = guard.finish();
306 assert_eq!(&*tail.samples, &zeros);
307 assert!(tail.event.is_none());
308 assert!(guard.finish().samples.is_empty());
309 Ok(())
310 }
311 #[test]
312 fn discarded_startup_loss_retains_native_scalar_count_and_validates_frames() -> Result<()> {
313 let loss = discarded_startup_loss(&[0.25; 26], 44_100, 2)?
314 .expect("positive discarded startup prefix");
315 assert_eq!(loss.samples().map(NonZeroU64::get), Some(26));
316 assert_eq!((loss.sample_rate(), loss.channels()), (44_100, 2));
317 assert_eq!(loss.reason(), crate::LossReason::GenerationEnded);
318 assert!(discarded_startup_loss(&[], 48_000, 2)?.is_none());
319 for (samples, rate, channels) in [
320 (&[0.25][..], 48_000, 2),
321 (&[][..], 0, 2),
322 (&[][..], 48_000, 0),
323 ] {
324 assert!(matches!(
325 discarded_startup_loss(samples, rate, channels),
326 Err(Error::UnsupportedFormat(_))
327 ));
328 }
329 Ok(())
330 }
331}