1use 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 pub fn take_completed(self) -> Option<RecordedAudio> {
351 self.take_completed.call(())
352 }
353}
354
355pub 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}