Skip to main content

dioxus_audio/
recorder.rs

1//! Microphone recording state and hooks.
2
3use std::fmt;
4use std::time::Duration;
5
6use dioxus::prelude::*;
7
8use crate::AudioError;
9use crate::analysis::AudioAnalyser;
10use crate::devices::MicrophonePermission;
11use crate::{AudioInputId, RecordedAudio};
12
13#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
14mod web;
15
16#[derive(Clone, Debug, PartialEq, Eq)]
17#[non_exhaustive]
18pub enum RecorderStatus {
19    Idle,
20    RequestingPermission,
21    Recording,
22    Paused,
23    Stopping,
24    Failed(AudioError),
25}
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub enum CompletionDisposition {
29    Save,
30    Discard,
31}
32
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34pub struct RecordingSessionId(u64);
35
36#[derive(Clone, Debug, PartialEq, Eq)]
37pub struct RecorderCommandError {
38    message: &'static str,
39}
40
41impl RecorderCommandError {
42    pub fn message(&self) -> &'static str {
43        self.message
44    }
45}
46
47impl fmt::Display for RecorderCommandError {
48    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
49        formatter.write_str(self.message)
50    }
51}
52
53impl std::error::Error for RecorderCommandError {}
54
55#[derive(Debug)]
56pub struct RecorderLifecycle {
57    status: RecorderStatus,
58    generation: u64,
59    active_session: Option<RecordingSessionId>,
60    completion: CompletionDisposition,
61    finalizing: bool,
62    has_unconsumed_recording: bool,
63}
64
65impl Default for RecorderLifecycle {
66    fn default() -> Self {
67        Self {
68            status: RecorderStatus::Idle,
69            generation: 0,
70            active_session: None,
71            completion: CompletionDisposition::Save,
72            finalizing: false,
73            has_unconsumed_recording: false,
74        }
75    }
76}
77
78impl RecorderLifecycle {
79    pub fn status(&self) -> &RecorderStatus {
80        &self.status
81    }
82
83    pub fn start(&mut self) -> Result<RecordingSessionId, RecorderCommandError> {
84        if self.has_unconsumed_recording {
85            return Err(command_error(
86                "clear the completed recording before starting another",
87            ));
88        }
89        if !matches!(
90            self.status,
91            RecorderStatus::Idle | RecorderStatus::Failed(_)
92        ) {
93            return Err(command_error("recording is already active"));
94        }
95
96        self.generation = self.generation.wrapping_add(1);
97        let session = RecordingSessionId(self.generation);
98        self.active_session = Some(session);
99        self.completion = CompletionDisposition::Save;
100        self.finalizing = false;
101        self.status = RecorderStatus::RequestingPermission;
102        Ok(session)
103    }
104
105    pub fn started(&mut self, session: RecordingSessionId) -> bool {
106        if self.active_session == Some(session)
107            && matches!(self.status, RecorderStatus::RequestingPermission)
108        {
109            self.status = RecorderStatus::Recording;
110            true
111        } else {
112            false
113        }
114    }
115
116    pub fn stop(&mut self) -> Result<(), RecorderCommandError> {
117        if !matches!(
118            self.status,
119            RecorderStatus::Recording | RecorderStatus::Paused
120        ) {
121            return Err(command_error(
122                "recording cannot be stopped in its current state",
123            ));
124        }
125
126        self.completion = CompletionDisposition::Save;
127        self.status = RecorderStatus::Stopping;
128        Ok(())
129    }
130
131    pub fn pause(&mut self) -> Result<(), RecorderCommandError> {
132        if !matches!(self.status, RecorderStatus::Recording) {
133            return Err(command_error("recording can only be paused while active"));
134        }
135        self.status = RecorderStatus::Paused;
136        Ok(())
137    }
138
139    pub fn resume(&mut self) -> Result<(), RecorderCommandError> {
140        if !matches!(self.status, RecorderStatus::Paused) {
141            return Err(command_error("recording can only be resumed while paused"));
142        }
143        self.status = RecorderStatus::Recording;
144        Ok(())
145    }
146
147    pub fn cancel(&mut self) -> Result<(), RecorderCommandError> {
148        match self.status {
149            RecorderStatus::RequestingPermission => {
150                self.active_session = None;
151                self.status = RecorderStatus::Idle;
152            }
153            RecorderStatus::Recording | RecorderStatus::Paused => {
154                self.completion = CompletionDisposition::Discard;
155                self.status = RecorderStatus::Stopping;
156            }
157            _ => {
158                return Err(command_error(
159                    "recording cannot be cancelled in its current state",
160                ));
161            }
162        }
163
164        Ok(())
165    }
166
167    pub fn begin_finalize(&mut self, session: RecordingSessionId) -> Option<CompletionDisposition> {
168        if self.active_session != Some(session) || self.finalizing {
169            return None;
170        }
171
172        match self.status {
173            RecorderStatus::Stopping => {}
174            RecorderStatus::Recording | RecorderStatus::Paused => {
175                self.completion = CompletionDisposition::Save;
176                self.status = RecorderStatus::Stopping;
177            }
178            _ => return None,
179        }
180
181        self.finalizing = true;
182        Some(self.completion)
183    }
184
185    pub fn complete_finalize(&mut self, session: RecordingSessionId) -> bool {
186        if self.active_session != Some(session) || !self.finalizing {
187            return false;
188        }
189
190        self.active_session = None;
191        self.finalizing = false;
192        self.has_unconsumed_recording = self.completion == CompletionDisposition::Save;
193        self.status = RecorderStatus::Idle;
194        true
195    }
196
197    pub fn clear_completed(&mut self) {
198        self.has_unconsumed_recording = false;
199    }
200
201    pub fn failed(&mut self, session: RecordingSessionId, error: AudioError) -> bool {
202        if self.active_session != Some(session) {
203            return false;
204        }
205
206        self.active_session = None;
207        self.finalizing = false;
208        self.status = RecorderStatus::Failed(error);
209        true
210    }
211
212    pub fn configuration_failed(&mut self, error: AudioError) -> bool {
213        if self.active_session.is_some()
214            || !matches!(
215                self.status,
216                RecorderStatus::Idle | RecorderStatus::Failed(_)
217            )
218        {
219            return false;
220        }
221        self.status = RecorderStatus::Failed(error);
222        true
223    }
224}
225
226fn command_error(message: &'static str) -> RecorderCommandError {
227    RecorderCommandError { message }
228}
229
230#[derive(Clone, Debug, PartialEq)]
231#[non_exhaustive]
232pub struct RecorderOptions {
233    pub fft_size: u32,
234    pub smoothing: f64,
235    pub peak_interval: Duration,
236    pub mime_types: Vec<String>,
237    pub audio_bits_per_second: Option<u32>,
238}
239
240impl Default for RecorderOptions {
241    fn default() -> Self {
242        Self {
243            fft_size: 256,
244            smoothing: 0.8,
245            peak_interval: Duration::from_millis(100),
246            mime_types: vec![
247                "audio/webm;codecs=opus".to_string(),
248                "audio/webm".to_string(),
249                "audio/mp4".to_string(),
250            ],
251            audio_bits_per_second: None,
252        }
253    }
254}
255
256impl RecorderOptions {
257    pub fn validate(&self) -> Result<(), AudioError> {
258        if !(32..=32768).contains(&self.fft_size) || !self.fft_size.is_power_of_two() {
259            return Err(AudioError::new(
260                crate::AudioErrorKind::InvalidConfiguration,
261                "fft_size must be a power of two between 32 and 32768",
262            ));
263        }
264        if !(0.0..=1.0).contains(&self.smoothing) {
265            return Err(AudioError::new(
266                crate::AudioErrorKind::InvalidConfiguration,
267                "smoothing must be between 0 and 1",
268            ));
269        }
270        if self.peak_interval.is_zero() {
271            return Err(AudioError::new(
272                crate::AudioErrorKind::InvalidConfiguration,
273                "peak_interval must be greater than zero",
274            ));
275        }
276        Ok(())
277    }
278}
279
280#[derive(Clone, Debug, PartialEq, Eq)]
281pub struct MicrophoneStatus {
282    pub permission: MicrophonePermission,
283    pub recorder: RecorderStatus,
284    pub input_device: Option<AudioInputId>,
285    pub muted: bool,
286}
287
288#[derive(Clone, Copy, PartialEq)]
289pub struct AudioRecorder {
290    status: ReadSignal<RecorderStatus>,
291    completed: ReadSignal<Option<RecordedAudio>>,
292    analyser: ReadSignal<Option<AudioAnalyser>>,
293    elapsed: ReadSignal<Duration>,
294    microphone: ReadSignal<MicrophoneStatus>,
295    start: Callback<(), Result<(), RecorderCommandError>>,
296    pause: Callback<(), Result<(), RecorderCommandError>>,
297    resume: Callback<(), Result<(), RecorderCommandError>>,
298    stop: Callback<(), Result<(), RecorderCommandError>>,
299    cancel: Callback<(), Result<(), RecorderCommandError>>,
300    take_completed: Callback<(), Option<RecordedAudio>>,
301    clear_completed: Callback,
302}
303
304impl AudioRecorder {
305    pub fn status(self) -> ReadSignal<RecorderStatus> {
306        self.status
307    }
308
309    pub fn completed(self) -> ReadSignal<Option<RecordedAudio>> {
310        self.completed
311    }
312
313    pub fn analyser(self) -> ReadSignal<Option<AudioAnalyser>> {
314        self.analyser
315    }
316
317    pub fn elapsed(self) -> ReadSignal<Duration> {
318        self.elapsed
319    }
320
321    pub fn microphone(self) -> ReadSignal<MicrophoneStatus> {
322        self.microphone
323    }
324
325    pub fn start(self) -> Result<(), RecorderCommandError> {
326        self.start.call(())
327    }
328
329    pub fn pause(self) -> Result<(), RecorderCommandError> {
330        self.pause.call(())
331    }
332
333    pub fn resume(self) -> Result<(), RecorderCommandError> {
334        self.resume.call(())
335    }
336
337    pub fn stop(self) -> Result<(), RecorderCommandError> {
338        self.stop.call(())
339    }
340
341    pub fn cancel(self) -> Result<(), RecorderCommandError> {
342        self.cancel.call(())
343    }
344
345    pub fn clear_completed(self) {
346        self.clear_completed.call(());
347    }
348
349    /// Move the completed recording out without cloning its audio buffer.
350    pub fn take_completed(self) -> Option<RecordedAudio> {
351        self.take_completed.call(())
352    }
353}
354
355/// Create a recorder controller. The selected input is snapshotted by `start`.
356pub fn use_audio_recorder(
357    options: RecorderOptions,
358    selected_input: ReadSignal<Option<AudioInputId>>,
359) -> AudioRecorder {
360    #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
361    {
362        web::use_web_audio_recorder(options, selected_input)
363    }
364
365    #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
366    {
367        let _ = (options, selected_input);
368        use_unsupported_audio_recorder()
369    }
370}
371
372#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
373fn use_unsupported_audio_recorder() -> AudioRecorder {
374    let error = AudioError::unsupported();
375    let mut status = use_signal(|| RecorderStatus::Idle);
376    let completed = use_signal(|| None::<RecordedAudio>);
377    let analyser = use_signal(|| None::<AudioAnalyser>);
378    let elapsed = use_signal(|| Duration::ZERO);
379    let mut microphone = use_signal(|| MicrophoneStatus {
380        permission: MicrophonePermission::Unknown,
381        recorder: RecorderStatus::Idle,
382        input_device: None,
383        muted: false,
384    });
385    use_effect(move || {
386        let status_error = RecorderStatus::Failed(error.clone());
387        status.set(status_error.clone());
388        microphone.set(MicrophoneStatus {
389            permission: MicrophonePermission::Unsupported,
390            recorder: status_error,
391            input_device: None,
392            muted: false,
393        });
394    });
395    let unsupported: Callback<(), Result<(), RecorderCommandError>> = use_callback(|()| {
396        Err(command_error(
397            "audio recording is unsupported on this platform",
398        ))
399    });
400    let mut completed_to_clear = completed;
401    let clear_completed = use_callback(move |()| completed_to_clear.set(None));
402    let mut completed_to_take = completed;
403    let take_completed = use_callback(move |()| completed_to_take.write().take());
404
405    AudioRecorder {
406        status: status.into(),
407        completed: completed.into(),
408        analyser: analyser.into(),
409        elapsed: elapsed.into(),
410        microphone: microphone.into(),
411        start: unsupported,
412        pause: unsupported,
413        resume: unsupported,
414        stop: unsupported,
415        cancel: unsupported,
416        take_completed,
417        clear_completed,
418    }
419}