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