Skip to main content

aria_kit/audio/
recording.rs

1//! Reading a recording while capture is still writing it.
2//!
3//! Capture appends samples from its own thread; whoever transcribes takes them
4//! as they arrive. A backend that needs the whole recording asks for it and
5//! waits. Streaming transcription asks for one chunk at a time and starts
6//! before capture ends.
7
8use std::ops::Range;
9use std::sync::{Arc, Condvar, Mutex, MutexGuard};
10use std::time::Duration;
11#[cfg(not(target_family = "wasm"))]
12use std::time::Instant;
13
14#[cfg(not(target_family = "wasm"))]
15use super::MAX_RECORDING_DURATION;
16use super::canonical_duration;
17#[cfg(not(target_family = "wasm"))]
18use super::spool::WavSpool;
19#[cfg(not(target_family = "wasm"))]
20use super::{MicrophoneError, SpoolError, WavError, WavReplayError};
21#[cfg(not(target_family = "wasm"))]
22use super::{f32_from_pcm16, pcm16_from_f32};
23
24// The source-specific error types stay with their backends.
25#[derive(Debug, thiserror::Error)]
26pub enum CaptureError {
27    #[cfg(not(target_family = "wasm"))]
28    #[error(transparent)]
29    Microphone(#[from] MicrophoneError),
30
31    #[cfg(not(target_family = "wasm"))]
32    #[error(transparent)]
33    WavReplay(#[from] WavReplayError),
34
35    #[cfg(not(target_family = "wasm"))]
36    #[error(transparent)]
37    Wav(#[from] WavError),
38
39    #[cfg(not(target_family = "wasm"))]
40    #[error(transparent)]
41    Spool(#[from] SpoolError),
42
43    #[error("no speech was detected within {0:?}")]
44    NoSpeech(Duration),
45
46    #[error("recording exceeded its safety deadline of {0:?}")]
47    DeadlineExceeded(Duration),
48
49    #[error("the capture source stopped before the recording ended")]
50    SourceGone,
51
52    #[error("the recording already failed and its cause was reported")]
53    AlreadyFailed,
54
55    #[cfg(target_family = "wasm")]
56    #[error("the recording is still capturing and this target cannot wait for it")]
57    StillCapturing,
58}
59
60/// The result of a finished recording: its length and, on a target with a
61/// filesystem, its WAV. A WAV this crate wrote is deleted when its owner
62/// drops. A caller's own file is only borrowed. A recording ended by
63/// [`RecordingWriter::finish`] has none.
64pub struct RecordedAudio {
65    #[cfg(not(target_family = "wasm"))]
66    storage: Storage,
67    samples: u64,
68}
69
70#[cfg(not(target_family = "wasm"))]
71enum Storage {
72    Owned(tempfile::TempPath),
73    Borrowed(std::path::PathBuf),
74    Unsaved,
75}
76
77impl RecordedAudio {
78    pub fn duration(&self) -> Duration {
79        canonical_duration(self.samples)
80    }
81
82    fn from_sample_count(samples: u64) -> Self {
83        Self {
84            #[cfg(not(target_family = "wasm"))]
85            storage: Storage::Unsaved,
86            samples,
87        }
88    }
89}
90
91#[cfg(not(target_family = "wasm"))]
92impl RecordedAudio {
93    pub(super) fn owned(path: tempfile::TempPath, samples: u64) -> Self {
94        Self {
95            storage: Storage::Owned(path),
96            samples,
97        }
98    }
99
100    pub(super) fn borrowed(path: std::path::PathBuf, samples: u64) -> Self {
101        Self {
102            storage: Storage::Borrowed(path),
103            samples,
104        }
105    }
106
107    /// `None` for a recording ended by [`RecordingWriter::finish`], which
108    /// never touched the filesystem.
109    pub fn path(&self) -> Option<&std::path::Path> {
110        match &self.storage {
111            Storage::Owned(path) => Some(path.as_ref()),
112            Storage::Borrowed(path) => Some(path),
113            Storage::Unsaved => None,
114        }
115    }
116}
117
118/// The two ends of one recording, before capture begins.
119pub(super) fn ends() -> (RecordingReader, RecordingWriter) {
120    let recording = Arc::new(Recording::new());
121    let (queue, _from) = recording.enrol();
122    let reader = RecordingReader {
123        recording: Arc::clone(&recording),
124        samples: Vec::new(),
125        offset: 0,
126        keep_samples: true,
127        queue,
128        owns_outcome: true,
129        outcome: None,
130    };
131    (reader, RecordingWriter { recording })
132}
133
134/// What one capture's writer and readers share: each reader's queue of
135/// unread samples, the count written, and how capture ended. It keeps no
136/// history: a sample lives only in the queues of readers yet to take it.
137pub struct Recording {
138    state: Mutex<State>,
139    changed: Condvar,
140}
141
142#[derive(Default)]
143struct State {
144    /// Capture is live: audio is flowing, whether or not any is kept yet.
145    capturing: bool,
146    /// One queue per live reader. Capture writes into every one of them, so a
147    /// reader falling behind holds its own memory and nobody else's.
148    queues: Vec<Queue>,
149    /// Counts up, so an id stays unique after other queues retire.
150    next_queue: u64,
151    /// Samples written so far. Stream positions count from the first.
152    appended: usize,
153    ended: Option<Ended>,
154}
155
156/// What capture has written that one reader has not taken yet.
157struct Queue {
158    id: u64,
159    pending: Vec<f32>,
160}
161
162impl State {
163    /// One reader's queue. A live `RecordingReader` always has one — its
164    /// `Drop` is what retires it — so a missing queue is a bug rather than an
165    /// ending.
166    fn queue(&mut self, id: u64) -> &mut Queue {
167        self.queues
168            .iter_mut()
169            .find(|queue| queue.id == id)
170            .expect("a live reader holds its own queue")
171    }
172}
173
174struct Ended {
175    /// Whether capture failed. Forks read this: the owning reader moves
176    /// `outcome` out, leaving `AlreadyFailed` in its place either way.
177    failed: bool,
178    outcome: Result<RecordedAudio, CaptureError>,
179    #[cfg(not(target_family = "wasm"))]
180    at: Instant,
181}
182
183/// Room for `samples` plus a second, within the longest spool allowed.
184#[cfg(not(target_family = "wasm"))]
185fn spool_room(samples: usize) -> Duration {
186    (canonical_duration(samples as u64) + Duration::from_secs(1)).min(MAX_RECORDING_DURATION)
187}
188
189impl Recording {
190    /// A new recording's two ends. Capture is live at once.
191    pub fn open() -> (RecordingReader, RecordingWriter) {
192        let (reader, writer) = ends();
193        writer.begin();
194        (reader, writer)
195    }
196}
197
198/// The write end of a [`Recording`].
199///
200/// Dropping this without [`RecordingWriter::finish`] ends the recording the
201/// way a vanished capture thread does.
202pub struct RecordingWriter {
203    recording: Arc<Recording>,
204}
205
206impl RecordingWriter {
207    pub fn push(&mut self, samples: &[f32]) {
208        self.recording.extend(samples.iter().copied());
209    }
210
211    /// Ends the recording. A reader sees every pushed sample.
212    pub fn finish(self) {
213        let appended = self.recording.lock().appended as u64;
214        self.close(Ok(RecordedAudio::from_sample_count(appended)));
215    }
216
217    #[cfg(not(target_family = "wasm"))]
218    pub(super) fn recording(&self) -> Arc<Recording> {
219        Arc::clone(&self.recording)
220    }
221
222    #[cfg(not(target_family = "wasm"))]
223    pub(super) fn push_pcm16(&self, samples: &[i16]) {
224        self.recording.append(samples);
225    }
226
227    /// Capture is live from here on. The first sample can come later — a
228    /// microphone holds back leading silence — so this mark is when the
229    /// recording started.
230    pub(super) fn begin(&self) {
231        self.recording.begin();
232    }
233
234    pub(super) fn close(&self, outcome: Result<RecordedAudio, CaptureError>) {
235        self.recording.close(outcome);
236    }
237}
238
239impl Drop for RecordingWriter {
240    fn drop(&mut self) {
241        self.recording.close(Err(CaptureError::SourceGone));
242    }
243}
244
245impl Recording {
246    fn new() -> Self {
247        Self {
248            state: Mutex::default(),
249            changed: Condvar::new(),
250        }
251    }
252
253    fn begin(&self) {
254        let mut state = self.lock();
255        state.capturing = true;
256        drop(state);
257        self.changed.notify_all();
258    }
259
260    /// Hands the readers 16-bit PCM samples as floats.
261    #[cfg(not(target_family = "wasm"))]
262    pub(super) fn append(&self, samples: &[i16]) {
263        self.extend(samples.iter().copied().map(f32_from_pcm16));
264    }
265
266    fn extend(&self, samples: impl ExactSizeIterator<Item = f32>) {
267        let mut state = self.lock();
268        if state.ended.is_some() {
269            return;
270        }
271        let count = samples.len();
272        state.appended += count;
273        // Written once into the first queue, then copied across from there,
274        // so the iterator is walked once however many readers there are.
275        if let Some((first, rest)) = state.queues.split_first_mut() {
276            first.pending.extend(samples);
277            let written = first.pending.len().saturating_sub(count);
278            for queue in rest {
279                queue.pending.extend_from_slice(&first.pending[written..]);
280            }
281        }
282        drop(state);
283        self.changed.notify_all();
284    }
285
286    /// Opens a queue for one reader, and says where it opened: the recording
287    /// keeps no history, so a reader enrolled late begins where capture stands.
288    ///
289    /// Both under one lock. Read separately, a write landing between them
290    /// would go into the new queue and be counted in the origin too, which
291    /// shifts every position that reader ever reports.
292    fn enrol(&self) -> (u64, u64) {
293        let mut state = self.lock();
294        let id = state.next_queue;
295        state.next_queue += 1;
296        state.queues.push(Queue {
297            id,
298            pending: Vec::new(),
299        });
300        (id, state.appended as u64)
301    }
302
303    /// Closes a reader's queue, so capture stops copying into it.
304    fn retire(&self, queue: u64) {
305        self.lock().queues.retain(|held| held.id != queue);
306    }
307
308    /// The first outcome stands, so a reader learns why capture stopped rather
309    /// than what tidied up after it.
310    fn close(&self, outcome: Result<RecordedAudio, CaptureError>) {
311        let mut state = self.lock();
312        if state.ended.is_none() {
313            state.ended = Some(Ended {
314                failed: outcome.is_err(),
315                outcome,
316                #[cfg(not(target_family = "wasm"))]
317                at: Instant::now(),
318            });
319        }
320        drop(state);
321        self.changed.notify_all();
322    }
323
324    fn lock(&self) -> MutexGuard<'_, State> {
325        self.state
326            .lock()
327            .unwrap_or_else(|poisoned| poisoned.into_inner())
328    }
329}
330
331/// A window onto a recording, readable while capture is still writing it.
332/// One reader; [`RecordingReader::fork`] adds another.
333///
334/// The window holds what this reader has collected and not yet released.
335/// Audio is addressed by stream position — counted from the first sample
336/// capture ever appended — so a position keeps meaning what it meant after a
337/// release moves the window forward.
338pub struct RecordingReader {
339    recording: Arc<Recording>,
340    samples: Vec<f32>,
341    /// Stream position of `samples[0]`.
342    offset: u64,
343    /// Cleared by a reader that only wants the WAV, so a recording nobody
344    /// reads sample by sample is not held in memory twice.
345    keep_samples: bool,
346    /// Which queue in the recording this reader drains.
347    queue: u64,
348    /// Whether the finished WAV and the failure cause are this reader's to
349    /// take. Exactly one reader per recording owns them.
350    owns_outcome: bool,
351    outcome: Option<Ending>,
352}
353
354/// How the recording ended, for one reader. A fork learns the end and
355/// whether capture failed, but gets neither the audio nor the cause.
356enum Ending {
357    Recorded(Option<RecordedAudio>),
358    Failed,
359}
360
361impl RecordingReader {
362    /// A finished recording of `samples`, held in memory.
363    pub fn from_samples(samples: &[f32]) -> Self {
364        let (reader, mut writer) = Recording::open();
365        writer.push(samples);
366        writer.finish();
367        reader
368    }
369
370    /// A finished recording of `samples`, saved as a private WAV in
371    /// `spool_dir`.
372    #[cfg(not(target_family = "wasm"))]
373    pub fn from_samples_in(
374        spool_dir: &std::path::Path,
375        samples: &[f32],
376    ) -> Result<Self, CaptureError> {
377        let pcm16: Vec<i16> = samples.iter().copied().map(pcm16_from_f32).collect();
378        Self::from_pcm16_in(spool_dir, &pcm16)
379    }
380
381    /// [`RecordingReader::from_samples_in`] for samples already in 16-bit PCM.
382    #[cfg(not(target_family = "wasm"))]
383    pub fn from_pcm16_in(
384        spool_dir: &std::path::Path,
385        samples: &[i16],
386    ) -> Result<Self, CaptureError> {
387        let (recording, writer) = ends();
388        let mut spool = WavSpool::create(spool_dir, spool_room(samples.len()), &writer)?;
389        spool.write_samples(samples)?;
390        writer.close(spool.finish().map_err(CaptureError::from));
391        Ok(recording)
392    }
393
394    /// Waits until capture is live or the recording has ended, and reports
395    /// how a recording that ended before capturing failed.
396    #[cfg(not(target_family = "wasm"))]
397    pub(crate) fn wait_for_capture(&mut self) -> Result<(), CaptureError> {
398        let mut state = self.recording.lock();
399        while !state.capturing && state.ended.is_none() {
400            state = self.wait_for_more(state)?;
401        }
402        let capturing = state.capturing;
403        drop(state);
404        if capturing { Ok(()) } else { self.fill() }
405    }
406
407    /// Waits until the window reaches `position`, or until capture ends short
408    /// of it. Returns whether it reached `position`.
409    pub fn fill_through(&mut self, position: u64) -> Result<bool, CaptureError> {
410        while self.outcome.is_none() && self.held_to() < position {
411            self.take()?;
412        }
413        self.failure()?;
414        Ok(self.held_to() >= position)
415    }
416
417    /// Waits for capture to end.
418    pub fn fill(&mut self) -> Result<(), CaptureError> {
419        while self.outcome.is_none() {
420            self.take()?;
421        }
422        self.failure()
423    }
424
425    /// A second reader of the same recording, with its own window and its own
426    /// release floor: what one reader frees, the other still holds.
427    ///
428    /// The recording keeps no history, so a fork opens where capture stands
429    /// rather than at the beginning — ask [`RecordingReader::held_from`] where
430    /// that was.
431    ///
432    /// Every sample written from here on is copied into its queue until it
433    /// drops, so a fork nobody reads grows without bound.
434    pub fn fork(&self) -> Self {
435        let (queue, from) = self.recording.enrol();
436        Self {
437            recording: Arc::clone(&self.recording),
438            samples: Vec::new(),
439            offset: from,
440            keep_samples: true,
441            queue,
442            owns_outcome: false,
443            outcome: None,
444        }
445    }
446
447    /// Stream position of the first sample this reader still holds.
448    pub fn held_from(&self) -> u64 {
449        self.offset
450    }
451
452    /// Samples this reader has collected and not yet released.
453    pub fn samples(&self) -> &[f32] {
454        &self.samples
455    }
456
457    /// Stream position just past the last sample held.
458    pub fn held_to(&self) -> u64 {
459        self.offset + self.samples.len() as u64
460    }
461
462    /// The audio in `span`, clamped to the window. A span that ran off either
463    /// edge yields what survives of it, which may be nothing.
464    pub fn span(&self, span: Range<u64>) -> &[f32] {
465        let start = span.start.clamp(self.offset, self.held_to()) - self.offset;
466        let end = span.end.clamp(self.offset, self.held_to()) - self.offset;
467        &self.samples[start as usize..end.max(start) as usize]
468    }
469
470    /// Frees this reader's audio before `position`; other readers keep their
471    /// own copies.
472    pub fn release_before(&mut self, position: u64) {
473        let freed = position.clamp(self.offset, self.held_to()) - self.offset;
474        self.samples.drain(..freed as usize);
475        self.offset += freed;
476    }
477
478    /// Whether this reader has seen capture end. It learns that only when it
479    /// reads past the audio it holds, so it can lag.
480    /// [`RecordingReader::ended_at`] does not.
481    pub fn reached_end(&self) -> bool {
482        self.outcome.is_some()
483    }
484
485    /// The finished WAV. Waits for capture to end, and keeps none of the
486    /// samples it waits through.
487    ///
488    /// # Panics
489    ///
490    /// On a fork: only the reader that opened the recording owns its audio.
491    pub fn finished_audio(&mut self) -> Result<&RecordedAudio, CaptureError> {
492        assert!(
493            self.owns_outcome,
494            "only the reader that opened a recording has its audio"
495        );
496        self.keep_samples = false;
497        self.offset = self.held_to();
498        self.samples = Vec::new();
499        self.fill()?;
500        match &self.outcome {
501            Some(Ending::Recorded(Some(audio))) => Ok(audio),
502            _failed => Err(CaptureError::AlreadyFailed),
503        }
504    }
505
506    /// Waits up to `timeout` for capture to end, reading nothing. Returns
507    /// whether it ended.
508    #[cfg(not(target_family = "wasm"))]
509    #[must_use]
510    pub fn wait_for_end(&self, timeout: Duration) -> bool {
511        let state = self.recording.lock();
512        let (state, _timed_out) = self
513            .recording
514            .changed
515            .wait_timeout_while(state, timeout, |state| state.ended.is_none())
516            .unwrap_or_else(|poisoned| poisoned.into_inner());
517        state.ended.is_some()
518    }
519
520    /// When capture ended. Unlike [`RecordingReader::reached_end`], it does not wait
521    /// for this reader to catch up.
522    #[cfg(not(target_family = "wasm"))]
523    pub fn ended_at(&self) -> Option<Instant> {
524        self.recording.lock().ended.as_ref().map(|ended| ended.at)
525    }
526
527    #[cfg(not(target_family = "wasm"))]
528    fn wait_for_more<'state>(
529        &self,
530        state: MutexGuard<'state, State>,
531    ) -> Result<MutexGuard<'state, State>, CaptureError> {
532        Ok(self
533            .recording
534            .changed
535            .wait(state)
536            .unwrap_or_else(|poisoned| poisoned.into_inner()))
537    }
538
539    /// Whoever feeds this recording runs on the reader's own thread, so
540    /// waiting would wait forever.
541    #[cfg(target_family = "wasm")]
542    fn wait_for_more<'state>(
543        &self,
544        _state: MutexGuard<'state, State>,
545    ) -> Result<MutexGuard<'state, State>, CaptureError> {
546        Err(CaptureError::StillCapturing)
547    }
548
549    /// How much audio capture has written, whether or not it has been read.
550    pub fn duration(&self) -> Duration {
551        canonical_duration(self.recording.lock().appended as u64)
552    }
553
554    /// Waits for capture to append or end.
555    fn take(&mut self) -> Result<(), CaptureError> {
556        let mut state = self.recording.lock();
557        // Another reader's queue filling wakes this one too, so the wait
558        // re-checks its own rather than trusting the notification.
559        while state.queue(self.queue).pending.is_empty() && state.ended.is_none() {
560            state = self.wait_for_more(state)?;
561        }
562        let pending = &mut state.queue(self.queue).pending;
563        if self.keep_samples {
564            self.samples.append(pending);
565        } else {
566            // Discarded audio still moves the window, so a position keeps
567            // pointing where it did.
568            self.offset += pending.len() as u64;
569            pending.clear();
570        }
571
572        let Some(ended) = &mut state.ended else {
573            return Ok(());
574        };
575        if !self.owns_outcome {
576            // The WAV and the cause belong to whoever opened the recording.
577            // A fork learns only that there is no more audio coming.
578            let failed = ended.failed;
579            drop(state);
580            self.outcome = Some(match failed {
581                true => Ending::Failed,
582                false => Ending::Recorded(None),
583            });
584            return match failed {
585                true => Err(CaptureError::AlreadyFailed),
586                false => Ok(()),
587            };
588        }
589        // The outcome moves to this reader, its only owner. Nothing reads the
590        // placeholder left behind.
591        let outcome = std::mem::replace(&mut ended.outcome, Err(CaptureError::AlreadyFailed));
592        drop(state);
593        match outcome {
594            Ok(audio) => {
595                self.outcome = Some(Ending::Recorded(Some(audio)));
596                Ok(())
597            }
598            Err(error) => {
599                self.outcome = Some(Ending::Failed);
600                Err(error)
601            }
602        }
603    }
604
605    fn failure(&self) -> Result<(), CaptureError> {
606        match &self.outcome {
607            Some(Ending::Failed) => Err(CaptureError::AlreadyFailed),
608            _live_or_recorded => Ok(()),
609        }
610    }
611}
612
613/// Retires this reader's queue, so capture stops copying audio nobody will
614/// read into it.
615impl Drop for RecordingReader {
616    fn drop(&mut self) {
617        self.recording.retire(self.queue);
618    }
619}
620
621#[cfg(test)]
622mod tests {
623    use super::super::capture::MicrophoneError;
624    use super::super::testing::{recorded, spooled};
625
626    use super::*;
627
628    const SAMPLES: [i16; 4] = [0, 16_384, -16_384, 32_767];
629
630    /// One recording fed a batch at a time from another thread, as capture feeds
631    /// one, so the reader blocks between batches.
632    fn streamed(batches: Vec<Vec<i16>>) -> (tempfile::TempDir, RecordingReader) {
633        let (directory, writer, mut spool, recording) = spooled();
634        std::thread::spawn(move || {
635            for batch in batches {
636                spool.write_samples(&batch).expect("samples are writable");
637            }
638            writer.close(spool.finish().map_err(CaptureError::from));
639        });
640        (directory, recording)
641    }
642
643    fn expected() -> Vec<f32> {
644        SAMPLES.iter().copied().map(f32_from_pcm16).collect()
645    }
646
647    #[test]
648    fn a_fork_reads_the_audio_the_first_reader_already_took() {
649        let (mut recording, mut capture) = Recording::open();
650        let mut forked = recording.fork();
651        capture.push(&[0.25, 0.5]);
652        capture.finish();
653
654        recording.fill().expect("the recording ends");
655        forked.fill().expect("the fork ends too");
656
657        assert_eq!(recording.samples(), [0.25, 0.5]);
658        assert_eq!(
659            forked.samples(),
660            [0.25, 0.5],
661            "one reader taking the audio must not empty it for the other"
662        );
663    }
664
665    #[test]
666    fn a_fork_opens_where_capture_stands_rather_than_at_the_beginning() {
667        let (mut recording, mut capture) = Recording::open();
668        capture.push(&[0.1, 0.2]);
669        recording.take().expect("the first batch arrives");
670        let mut forked = recording.fork();
671        capture.push(&[0.3]);
672        capture.finish();
673
674        forked.fill().expect("the fork ends");
675
676        assert_eq!(forked.held_from(), 2, "the fork opens past the spent audio");
677        assert_eq!(forked.samples(), [0.3]);
678        assert_eq!(
679            forked.held_to(),
680            3,
681            "and still counts from the first sample ever"
682        );
683    }
684
685    #[test]
686    fn a_fork_counts_from_where_its_own_audio_starts() {
687        // Enrolling the queue and reading the origin have to happen under one
688        // lock. Split them and a write landing between lands in the queue and
689        // in the origin both, shifting every position the fork reports — so
690        // this forks against a writer that is already running.
691        let (recording, mut capture) = Recording::open();
692        let writing = std::thread::spawn(move || {
693            for sample in 0..4_000 {
694                capture.push(&[sample as f32]);
695            }
696            capture.finish();
697        });
698
699        for _ in 0..400 {
700            let mut forked = recording.fork();
701            if forked.take().is_err() {
702                break;
703            }
704            // Each sample carries its own stream position as its value.
705            if let Some(&first) = forked.samples().first() {
706                assert_eq!(
707                    first as u64,
708                    forked.held_from(),
709                    "a fork's first sample must sit where the fork says it does"
710                );
711            }
712        }
713        writing.join().expect("capture finishes");
714    }
715
716    #[test]
717    fn releasing_on_one_reader_leaves_the_other_window_alone() {
718        let (mut recording, mut capture) = Recording::open();
719        let mut forked = recording.fork();
720        capture.push(&[0.1, 0.2, 0.3]);
721        capture.finish();
722        recording.fill().expect("the recording ends");
723        forked.fill().expect("the fork ends");
724
725        recording.release_before(2);
726
727        assert_eq!(
728            recording.samples(),
729            [0.3],
730            "the reader freed its own prefix"
731        );
732        assert_eq!(
733            forked.samples(),
734            [0.1, 0.2, 0.3],
735            "a track walking behind must still be able to read it"
736        );
737    }
738
739    #[test]
740    fn the_finished_audio_belongs_to_the_reader_that_opened_the_recording() {
741        let (_directory, mut recording) = recorded(&SAMPLES);
742        let mut forked = recording.fork();
743
744        forked.fill().expect("the fork sees a clean end");
745
746        assert!(
747            forked.reached_end(),
748            "the fork learns there is no more audio"
749        );
750        assert!(
751            recording.finished_audio().is_ok(),
752            "and leaves the WAV for whoever opened the recording"
753        );
754    }
755
756    #[test]
757    fn a_failed_capture_ends_a_fork_with_the_same_verdict() {
758        let (_directory, writer, _spool, recording) = spooled();
759        let mut forked = recording.fork();
760        writer.close(Err(CaptureError::SourceGone));
761
762        let failure = forked.fill().expect_err("a failed capture fails its fork");
763
764        assert!(matches!(failure, CaptureError::AlreadyFailed));
765    }
766
767    #[test]
768    fn a_dropped_fork_stops_costing_capture_anything() {
769        let (reader, mut capture) = Recording::open();
770        let forked = reader.fork();
771        assert_eq!(reader.recording.lock().queues.len(), 2);
772
773        drop(forked);
774        capture.push(&[0.1]);
775
776        let state = reader.recording.lock();
777        assert_eq!(
778            state.queues.len(),
779            1,
780            "capture still copies into a dead queue"
781        );
782        assert_eq!(state.queues[0].pending.len(), 1);
783    }
784
785    #[test]
786    fn two_readers_waiting_are_both_woken_by_one_write() {
787        let (mut recording, mut capture) = Recording::open();
788        let mut forked = recording.fork();
789        let waiting = std::thread::spawn(move || {
790            forked.fill_through(1).expect("the fork is woken");
791            forked.held_to()
792        });
793
794        capture.push(&[0.5]);
795        recording.fill_through(1).expect("the reader is woken");
796
797        assert_eq!(recording.held_to(), 1);
798        assert_eq!(waiting.join().expect("the fork thread finishes"), 1);
799    }
800
801    #[test]
802    fn a_reader_waits_for_samples_capture_has_not_written_yet() {
803        let batches = SAMPLES.iter().map(|&sample| vec![sample]).collect();
804        let (_directory, mut recording) = streamed(batches);
805
806        recording.fill().expect("the recording ends");
807
808        assert_eq!(recording.samples(), expected());
809    }
810
811    #[test]
812    fn a_reader_can_take_a_prefix_before_the_recording_ends() {
813        let (_directory, mut recording) = recorded(&SAMPLES);
814
815        recording
816            .fill_through(2)
817            .expect("two samples are available");
818        let first = recording.samples()[..2].to_vec();
819        recording.fill().expect("the rest is available");
820
821        assert_eq!(first, expected()[..2]);
822        assert_eq!(recording.samples(), expected());
823    }
824
825    #[test]
826    fn asking_past_the_end_returns_what_was_recorded() {
827        let (_directory, mut recording) = recorded(&SAMPLES);
828
829        recording
830            .fill_through(SAMPLES.len() as u64 * 4)
831            .expect("a short recording is not a failure");
832
833        assert!(recording.reached_end());
834        assert_eq!(recording.samples(), expected());
835    }
836
837    /// The invariant everything else rests on: the reader and the WAV are fed
838    /// by the same write, so they can never disagree.
839    #[test]
840    fn what_the_reader_sees_is_what_the_wav_got() {
841        let batches = SAMPLES.iter().map(|&sample| vec![sample]).collect();
842        let (_directory, mut recording) = streamed(batches);
843        recording.fill().expect("the recording ends");
844        let read = recording.samples().to_vec();
845
846        let audio = recording.finished_audio().expect("the recording succeeded");
847        let written: Vec<f32> = hound::WavReader::open(audio.path().expect("a captured WAV"))
848            .expect("the WAV is readable")
849            .into_samples::<i16>()
850            .map(|sample| f32_from_pcm16(sample.expect("every sample reads back")))
851            .collect();
852
853        assert_eq!(read, written);
854    }
855
856    #[test]
857    fn a_recording_as_long_as_allowed_still_gets_a_spool() {
858        let samples = 15 * 60 * crate::audio::CANONICAL_SAMPLE_RATE as usize;
859        assert!(super::super::spool::maximum_samples(spool_room(samples)).is_ok());
860    }
861
862    #[test]
863    fn a_recording_longer_than_allowed_is_refused_and_leaves_no_file() {
864        let directory = tempfile::tempdir().expect("tempdir must be creatable");
865        let samples = 15 * 60 * crate::audio::CANONICAL_SAMPLE_RATE as usize + 1;
866
867        let error = RecordingReader::from_pcm16_in(directory.path(), &vec![0; samples])
868            .err()
869            .expect("an oversized recording is refused");
870
871        assert!(matches!(
872            error,
873            CaptureError::Spool(SpoolError::LimitExceeded { .. })
874        ));
875        let leftovers = std::fs::read_dir(directory.path()).expect("the directory lists");
876        assert_eq!(leftovers.count(), 0);
877    }
878
879    #[test]
880    fn a_fill_through_says_whether_it_reached_the_position() {
881        let mut recording = RecordingReader::from_samples(&[0.0; 4]);
882
883        assert!(recording.fill_through(4).expect("the recording succeeded"));
884        assert!(!recording.fill_through(5).expect("the recording succeeded"));
885    }
886
887    #[test]
888    fn waiting_for_the_end_times_out_while_capture_runs() {
889        let (recording, writer) = Recording::open();
890
891        assert!(!recording.wait_for_end(Duration::from_millis(10)));
892        writer.finish();
893        assert!(recording.wait_for_end(Duration::from_millis(10)));
894    }
895
896    #[test]
897    fn a_recording_from_samples_holds_them_and_has_ended() {
898        let mut recording = RecordingReader::from_samples(&[0.25, -0.5]);
899
900        recording.fill().expect("the recording already ended");
901
902        assert_eq!(recording.samples(), [0.25, -0.5]);
903        assert_eq!(
904            recording.finished_audio().expect("it succeeded").path(),
905            None
906        );
907    }
908
909    #[test]
910    fn a_pcm16_recording_keeps_every_sample_exactly() {
911        let directory = tempfile::tempdir().expect("test directory is created");
912        let mut recording = RecordingReader::from_pcm16_in(directory.path(), &SAMPLES)
913            .expect("the recording is written");
914
915        let audio = recording.finished_audio().expect("the recording succeeded");
916        let written: Vec<i16> = hound::WavReader::open(audio.path().expect("a saved WAV"))
917            .expect("the WAV is readable")
918            .into_samples::<i16>()
919            .map(|sample| sample.expect("every sample reads back"))
920            .collect();
921
922        assert_eq!(written, SAMPLES);
923    }
924
925    #[test]
926    fn the_wav_reader_keeps_none_of_the_samples_it_waits_through() {
927        let (_directory, mut recording) = recorded(&SAMPLES);
928
929        recording.finished_audio().expect("the recording succeeded");
930
931        assert!(recording.samples().is_empty());
932    }
933
934    /// Reading on past a failure must not look like an empty recording that
935    /// simply had nothing in it.
936    #[test]
937    fn a_failed_recording_stays_failed_for_every_later_read() {
938        let (mut recording, writer) = ends();
939        writer.close(Err(CaptureError::Microphone(
940            MicrophoneError::NoInputDevice,
941        )));
942        recording
943            .fill()
944            .expect_err("the first read reports the cause");
945
946        let again = recording.fill_through(1).expect_err("so does the next one");
947        let finished = recording
948            .finished_audio()
949            .err()
950            .expect("and so does the WAV");
951
952        assert!(matches!(again, CaptureError::AlreadyFailed));
953        assert!(matches!(finished, CaptureError::AlreadyFailed));
954    }
955
956    /// Silence appends no samples, so capture being live must not depend on
957    /// any sample arriving.
958    #[test]
959    fn a_silent_capture_still_counts_as_started() {
960        let (mut recording, writer) = ends();
961        writer.begin();
962
963        recording
964            .wait_for_capture()
965            .expect("a live capture has started");
966
967        assert!(recording.samples().is_empty());
968    }
969
970    #[test]
971    fn a_capture_that_dies_before_starting_reports_its_cause() {
972        let (mut recording, writer) = ends();
973        writer.close(Err(CaptureError::Microphone(
974            MicrophoneError::NoInputDevice,
975        )));
976
977        let error = recording
978            .wait_for_capture()
979            .expect_err("the recording never started");
980
981        assert!(matches!(
982            error,
983            CaptureError::Microphone(MicrophoneError::NoInputDevice)
984        ));
985    }
986
987    #[test]
988    fn dropping_the_capture_end_ends_the_recording() {
989        let (mut recording, writer) = ends();
990        drop(writer);
991
992        let error = recording
993            .fill()
994            .expect_err("a reader must not wait on a producer that is gone");
995
996        assert!(matches!(error, CaptureError::SourceGone));
997    }
998
999    #[test]
1000    fn the_first_outcome_is_the_one_the_reader_learns() {
1001        let (mut recording, writer) = ends();
1002        writer.close(Err(CaptureError::Microphone(
1003            MicrophoneError::NoInputDevice,
1004        )));
1005        drop(writer);
1006
1007        let error = recording.fill().expect_err("the recording failed to start");
1008
1009        assert!(matches!(
1010            error,
1011            CaptureError::Microphone(MicrophoneError::NoInputDevice)
1012        ));
1013    }
1014}