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    /// Stream position just past the last sample capture has written, whether
550    /// or not this reader has taken it.
551    pub fn written(&self) -> u64 {
552        self.recording.lock().appended as u64
553    }
554
555    /// How much audio capture has written, whether or not it has been read.
556    pub fn duration(&self) -> Duration {
557        canonical_duration(self.written())
558    }
559
560    /// Waits for capture to append or end.
561    fn take(&mut self) -> Result<(), CaptureError> {
562        let mut state = self.recording.lock();
563        // Another reader's queue filling wakes this one too, so the wait
564        // re-checks its own rather than trusting the notification.
565        while state.queue(self.queue).pending.is_empty() && state.ended.is_none() {
566            state = self.wait_for_more(state)?;
567        }
568        let pending = &mut state.queue(self.queue).pending;
569        if self.keep_samples {
570            self.samples.append(pending);
571        } else {
572            // Discarded audio still moves the window, so a position keeps
573            // pointing where it did.
574            self.offset += pending.len() as u64;
575            pending.clear();
576        }
577
578        let Some(ended) = &mut state.ended else {
579            return Ok(());
580        };
581        if !self.owns_outcome {
582            // The WAV and the cause belong to whoever opened the recording.
583            // A fork learns only that there is no more audio coming.
584            let failed = ended.failed;
585            drop(state);
586            self.outcome = Some(match failed {
587                true => Ending::Failed,
588                false => Ending::Recorded(None),
589            });
590            return match failed {
591                true => Err(CaptureError::AlreadyFailed),
592                false => Ok(()),
593            };
594        }
595        // The outcome moves to this reader, its only owner. Nothing reads the
596        // placeholder left behind.
597        let outcome = std::mem::replace(&mut ended.outcome, Err(CaptureError::AlreadyFailed));
598        drop(state);
599        match outcome {
600            Ok(audio) => {
601                self.outcome = Some(Ending::Recorded(Some(audio)));
602                Ok(())
603            }
604            Err(error) => {
605                self.outcome = Some(Ending::Failed);
606                Err(error)
607            }
608        }
609    }
610
611    fn failure(&self) -> Result<(), CaptureError> {
612        match &self.outcome {
613            Some(Ending::Failed) => Err(CaptureError::AlreadyFailed),
614            _live_or_recorded => Ok(()),
615        }
616    }
617}
618
619/// Retires this reader's queue, so capture stops copying audio nobody will
620/// read into it.
621impl Drop for RecordingReader {
622    fn drop(&mut self) {
623        self.recording.retire(self.queue);
624    }
625}
626
627#[cfg(test)]
628mod tests {
629    use super::super::capture::MicrophoneError;
630    use super::super::testing::{recorded, spooled};
631
632    use super::*;
633
634    const SAMPLES: [i16; 4] = [0, 16_384, -16_384, 32_767];
635
636    /// One recording fed a batch at a time from another thread, as capture feeds
637    /// one, so the reader blocks between batches.
638    fn streamed(batches: Vec<Vec<i16>>) -> (tempfile::TempDir, RecordingReader) {
639        let (directory, writer, mut spool, recording) = spooled();
640        std::thread::spawn(move || {
641            for batch in batches {
642                spool.write_samples(&batch).expect("samples are writable");
643            }
644            writer.close(spool.finish().map_err(CaptureError::from));
645        });
646        (directory, recording)
647    }
648
649    fn expected() -> Vec<f32> {
650        SAMPLES.iter().copied().map(f32_from_pcm16).collect()
651    }
652
653    #[test]
654    fn a_fork_reads_the_audio_the_first_reader_already_took() {
655        let (mut recording, mut capture) = Recording::open();
656        let mut forked = recording.fork();
657        capture.push(&[0.25, 0.5]);
658        capture.finish();
659
660        recording.fill().expect("the recording ends");
661        forked.fill().expect("the fork ends too");
662
663        assert_eq!(recording.samples(), [0.25, 0.5]);
664        assert_eq!(
665            forked.samples(),
666            [0.25, 0.5],
667            "one reader taking the audio must not empty it for the other"
668        );
669    }
670
671    #[test]
672    fn a_fork_opens_where_capture_stands_rather_than_at_the_beginning() {
673        let (mut recording, mut capture) = Recording::open();
674        capture.push(&[0.1, 0.2]);
675        recording.take().expect("the first batch arrives");
676        let mut forked = recording.fork();
677        capture.push(&[0.3]);
678        capture.finish();
679
680        forked.fill().expect("the fork ends");
681
682        assert_eq!(forked.held_from(), 2, "the fork opens past the spent audio");
683        assert_eq!(forked.samples(), [0.3]);
684        assert_eq!(
685            forked.held_to(),
686            3,
687            "and still counts from the first sample ever"
688        );
689    }
690
691    #[test]
692    fn a_fork_counts_from_where_its_own_audio_starts() {
693        // Enrolling the queue and reading the origin have to happen under one
694        // lock. Split them and a write landing between lands in the queue and
695        // in the origin both, shifting every position the fork reports — so
696        // this forks against a writer that is already running.
697        let (recording, mut capture) = Recording::open();
698        let writing = std::thread::spawn(move || {
699            for sample in 0..4_000 {
700                capture.push(&[sample as f32]);
701            }
702            capture.finish();
703        });
704
705        for _ in 0..400 {
706            let mut forked = recording.fork();
707            if forked.take().is_err() {
708                break;
709            }
710            // Each sample carries its own stream position as its value.
711            if let Some(&first) = forked.samples().first() {
712                assert_eq!(
713                    first as u64,
714                    forked.held_from(),
715                    "a fork's first sample must sit where the fork says it does"
716                );
717            }
718        }
719        writing.join().expect("capture finishes");
720    }
721
722    #[test]
723    fn releasing_on_one_reader_leaves_the_other_window_alone() {
724        let (mut recording, mut capture) = Recording::open();
725        let mut forked = recording.fork();
726        capture.push(&[0.1, 0.2, 0.3]);
727        capture.finish();
728        recording.fill().expect("the recording ends");
729        forked.fill().expect("the fork ends");
730
731        recording.release_before(2);
732
733        assert_eq!(
734            recording.samples(),
735            [0.3],
736            "the reader freed its own prefix"
737        );
738        assert_eq!(
739            forked.samples(),
740            [0.1, 0.2, 0.3],
741            "a track walking behind must still be able to read it"
742        );
743    }
744
745    #[test]
746    fn the_finished_audio_belongs_to_the_reader_that_opened_the_recording() {
747        let (_directory, mut recording) = recorded(&SAMPLES);
748        let mut forked = recording.fork();
749
750        forked.fill().expect("the fork sees a clean end");
751
752        assert!(
753            forked.reached_end(),
754            "the fork learns there is no more audio"
755        );
756        assert!(
757            recording.finished_audio().is_ok(),
758            "and leaves the WAV for whoever opened the recording"
759        );
760    }
761
762    #[test]
763    fn a_failed_capture_ends_a_fork_with_the_same_verdict() {
764        let (_directory, writer, _spool, recording) = spooled();
765        let mut forked = recording.fork();
766        writer.close(Err(CaptureError::SourceGone));
767
768        let failure = forked.fill().expect_err("a failed capture fails its fork");
769
770        assert!(matches!(failure, CaptureError::AlreadyFailed));
771    }
772
773    #[test]
774    fn a_dropped_fork_stops_costing_capture_anything() {
775        let (reader, mut capture) = Recording::open();
776        let forked = reader.fork();
777        assert_eq!(reader.recording.lock().queues.len(), 2);
778
779        drop(forked);
780        capture.push(&[0.1]);
781
782        let state = reader.recording.lock();
783        assert_eq!(
784            state.queues.len(),
785            1,
786            "capture still copies into a dead queue"
787        );
788        assert_eq!(state.queues[0].pending.len(), 1);
789    }
790
791    #[test]
792    fn two_readers_waiting_are_both_woken_by_one_write() {
793        let (mut recording, mut capture) = Recording::open();
794        let mut forked = recording.fork();
795        let waiting = std::thread::spawn(move || {
796            forked.fill_through(1).expect("the fork is woken");
797            forked.held_to()
798        });
799
800        capture.push(&[0.5]);
801        recording.fill_through(1).expect("the reader is woken");
802
803        assert_eq!(recording.held_to(), 1);
804        assert_eq!(waiting.join().expect("the fork thread finishes"), 1);
805    }
806
807    #[test]
808    fn a_reader_waits_for_samples_capture_has_not_written_yet() {
809        let batches = SAMPLES.iter().map(|&sample| vec![sample]).collect();
810        let (_directory, mut recording) = streamed(batches);
811
812        recording.fill().expect("the recording ends");
813
814        assert_eq!(recording.samples(), expected());
815    }
816
817    #[test]
818    fn a_reader_can_take_a_prefix_before_the_recording_ends() {
819        let (_directory, mut recording) = recorded(&SAMPLES);
820
821        recording
822            .fill_through(2)
823            .expect("two samples are available");
824        let first = recording.samples()[..2].to_vec();
825        recording.fill().expect("the rest is available");
826
827        assert_eq!(first, expected()[..2]);
828        assert_eq!(recording.samples(), expected());
829    }
830
831    #[test]
832    fn asking_past_the_end_returns_what_was_recorded() {
833        let (_directory, mut recording) = recorded(&SAMPLES);
834
835        recording
836            .fill_through(SAMPLES.len() as u64 * 4)
837            .expect("a short recording is not a failure");
838
839        assert!(recording.reached_end());
840        assert_eq!(recording.samples(), expected());
841    }
842
843    /// The invariant everything else rests on: the reader and the WAV are fed
844    /// by the same write, so they can never disagree.
845    #[test]
846    fn what_the_reader_sees_is_what_the_wav_got() {
847        let batches = SAMPLES.iter().map(|&sample| vec![sample]).collect();
848        let (_directory, mut recording) = streamed(batches);
849        recording.fill().expect("the recording ends");
850        let read = recording.samples().to_vec();
851
852        let audio = recording.finished_audio().expect("the recording succeeded");
853        let written: Vec<f32> = hound::WavReader::open(audio.path().expect("a captured WAV"))
854            .expect("the WAV is readable")
855            .into_samples::<i16>()
856            .map(|sample| f32_from_pcm16(sample.expect("every sample reads back")))
857            .collect();
858
859        assert_eq!(read, written);
860    }
861
862    #[test]
863    fn a_recording_as_long_as_allowed_still_gets_a_spool() {
864        let samples = 15 * 60 * crate::audio::CANONICAL_SAMPLE_RATE as usize;
865        assert!(super::super::spool::maximum_samples(spool_room(samples)).is_ok());
866    }
867
868    #[test]
869    fn a_recording_longer_than_allowed_is_refused_and_leaves_no_file() {
870        let directory = tempfile::tempdir().expect("tempdir must be creatable");
871        let samples = 15 * 60 * crate::audio::CANONICAL_SAMPLE_RATE as usize + 1;
872
873        let error = RecordingReader::from_pcm16_in(directory.path(), &vec![0; samples])
874            .err()
875            .expect("an oversized recording is refused");
876
877        assert!(matches!(
878            error,
879            CaptureError::Spool(SpoolError::LimitExceeded { .. })
880        ));
881        let leftovers = std::fs::read_dir(directory.path()).expect("the directory lists");
882        assert_eq!(leftovers.count(), 0);
883    }
884
885    #[test]
886    fn a_fill_through_says_whether_it_reached_the_position() {
887        let mut recording = RecordingReader::from_samples(&[0.0; 4]);
888
889        assert!(recording.fill_through(4).expect("the recording succeeded"));
890        assert!(!recording.fill_through(5).expect("the recording succeeded"));
891    }
892
893    #[test]
894    fn waiting_for_the_end_times_out_while_capture_runs() {
895        let (recording, writer) = Recording::open();
896
897        assert!(!recording.wait_for_end(Duration::from_millis(10)));
898        writer.finish();
899        assert!(recording.wait_for_end(Duration::from_millis(10)));
900    }
901
902    #[test]
903    fn a_recording_from_samples_holds_them_and_has_ended() {
904        let mut recording = RecordingReader::from_samples(&[0.25, -0.5]);
905
906        recording.fill().expect("the recording already ended");
907
908        assert_eq!(recording.samples(), [0.25, -0.5]);
909        assert_eq!(
910            recording.finished_audio().expect("it succeeded").path(),
911            None
912        );
913    }
914
915    #[test]
916    fn a_pcm16_recording_keeps_every_sample_exactly() {
917        let directory = tempfile::tempdir().expect("test directory is created");
918        let mut recording = RecordingReader::from_pcm16_in(directory.path(), &SAMPLES)
919            .expect("the recording is written");
920
921        let audio = recording.finished_audio().expect("the recording succeeded");
922        let written: Vec<i16> = hound::WavReader::open(audio.path().expect("a saved WAV"))
923            .expect("the WAV is readable")
924            .into_samples::<i16>()
925            .map(|sample| sample.expect("every sample reads back"))
926            .collect();
927
928        assert_eq!(written, SAMPLES);
929    }
930
931    #[test]
932    fn the_wav_reader_keeps_none_of_the_samples_it_waits_through() {
933        let (_directory, mut recording) = recorded(&SAMPLES);
934
935        recording.finished_audio().expect("the recording succeeded");
936
937        assert!(recording.samples().is_empty());
938    }
939
940    /// Reading on past a failure must not look like an empty recording that
941    /// simply had nothing in it.
942    #[test]
943    fn a_failed_recording_stays_failed_for_every_later_read() {
944        let (mut recording, writer) = ends();
945        writer.close(Err(CaptureError::Microphone(
946            MicrophoneError::NoInputDevice,
947        )));
948        recording
949            .fill()
950            .expect_err("the first read reports the cause");
951
952        let again = recording.fill_through(1).expect_err("so does the next one");
953        let finished = recording
954            .finished_audio()
955            .err()
956            .expect("and so does the WAV");
957
958        assert!(matches!(again, CaptureError::AlreadyFailed));
959        assert!(matches!(finished, CaptureError::AlreadyFailed));
960    }
961
962    /// Silence appends no samples, so capture being live must not depend on
963    /// any sample arriving.
964    #[test]
965    fn a_silent_capture_still_counts_as_started() {
966        let (mut recording, writer) = ends();
967        writer.begin();
968
969        recording
970            .wait_for_capture()
971            .expect("a live capture has started");
972
973        assert!(recording.samples().is_empty());
974    }
975
976    #[test]
977    fn a_capture_that_dies_before_starting_reports_its_cause() {
978        let (mut recording, writer) = ends();
979        writer.close(Err(CaptureError::Microphone(
980            MicrophoneError::NoInputDevice,
981        )));
982
983        let error = recording
984            .wait_for_capture()
985            .expect_err("the recording never started");
986
987        assert!(matches!(
988            error,
989            CaptureError::Microphone(MicrophoneError::NoInputDevice)
990        ));
991    }
992
993    #[test]
994    fn dropping_the_capture_end_ends_the_recording() {
995        let (mut recording, writer) = ends();
996        drop(writer);
997
998        let error = recording
999            .fill()
1000            .expect_err("a reader must not wait on a producer that is gone");
1001
1002        assert!(matches!(error, CaptureError::SourceGone));
1003    }
1004
1005    #[test]
1006    fn the_first_outcome_is_the_one_the_reader_learns() {
1007        let (mut recording, writer) = ends();
1008        writer.close(Err(CaptureError::Microphone(
1009            MicrophoneError::NoInputDevice,
1010        )));
1011        drop(writer);
1012
1013        let error = recording.fill().expect_err("the recording failed to start");
1014
1015        assert!(matches!(
1016            error,
1017            CaptureError::Microphone(MicrophoneError::NoInputDevice)
1018        ));
1019    }
1020}