1use alloc::vec::Vec;
18
19use azul_core::audio::{AudioConfig, AudioFrame};
20
21use super::capture_common::mic_backend;
22use azul_core::callbacks::Update;
23use azul_core::dom::{ComponentEventFilter, DatasetMergeCallbackType, Dom, EventFilter};
24use azul_core::refany::{OptionRefAny, RefAny};
25use azul_core::task::{ThreadId, ThreadReceiver};
26use azul_css::impl_option_inner; use azul_css::F32Vec;
28
29use crate::callbacks::{Callback, CallbackInfo, CallbackType};
30use crate::thread::{
31 Thread, ThreadCallback, ThreadReceiveMsg, ThreadSender, ThreadWriteBackMsg, WriteBackCallback,
32};
33
34pub type OnAudioFrameCallbackType = extern "C" fn(RefAny, CallbackInfo, AudioFrame) -> Update;
44impl_widget_callback!(
45 OnAudioFrame,
46 OptionOnAudioFrame,
47 OnAudioFrameCallback,
48 OnAudioFrameCallbackType
49);
50
51azul_core::impl_managed_callback! {
53 wrapper: OnAudioFrameCallback,
54 info_ty: CallbackInfo,
55 return_ty: Update,
56 default_ret: Update::DoNothing,
57 invoker_static: ON_AUDIO_FRAME_INVOKER,
58 invoker_ty: AzOnAudioFrameCallbackInvoker,
59 thunk_fn: az_on_audio_frame_callback_thunk,
60 setter_fn: AzApp_setOnAudioFrameCallbackInvoker,
61 from_handle_fn: AzOnAudioFrameCallback_createFromHostHandle,
62 extra_args: [ frame: AudioFrame ],
63}
64
65fn invoke_on_audio_frame(
68 hook: &OptionOnAudioFrame,
69 info: &CallbackInfo,
70 frame: AudioFrame,
71) -> Update {
72 match hook {
73 OptionOnAudioFrame::Some(h) => (h.callback.cb)(h.refany.clone(), *info, frame),
74 OptionOnAudioFrame::None => Update::DoNothing,
75 }
76}
77
78struct MicThreadInit {
80 sample_rate: u32,
81 channels: u16,
82}
83
84#[derive(Debug)]
87pub struct MicrophoneWidgetState {
88 pub config: AudioConfig,
90 pub started: bool,
92 pub on_frame: OptionOnAudioFrame,
95}
96
97#[repr(C)]
100#[derive(Debug)]
101pub struct MicrophoneWidget {
102 pub config: AudioConfig,
104 pub on_frame: OptionOnAudioFrame,
106}
107
108impl MicrophoneWidget {
109 #[must_use] pub const fn create(config: AudioConfig) -> Self {
111 Self {
112 config,
113 on_frame: OptionOnAudioFrame::None,
114 }
115 }
116
117 pub fn set_on_frame<C: Into<OnAudioFrameCallback>>(&mut self, data: RefAny, on_frame: C) {
121 self.on_frame = Some(OnAudioFrame {
122 refany: data,
123 callback: on_frame.into(),
124 })
125 .into();
126 }
127
128 #[must_use]
130 pub fn with_on_frame<C: Into<OnAudioFrameCallback>>(
131 mut self,
132 data: RefAny,
133 on_frame: C,
134 ) -> Self {
135 self.set_on_frame(data, on_frame);
136 self
137 }
138
139 #[must_use] pub fn dom(self) -> Dom {
143 let state = MicrophoneWidgetState {
144 config: self.config,
145 started: false,
146 on_frame: self.on_frame,
147 };
148 let dataset = RefAny::new(state);
149
150 Dom::create_div()
151 .with_dataset(OptionRefAny::Some(dataset.clone()))
152 .with_merge_callback(azul_core::dom::DatasetMergeCallback::from_ptr(merge_microphone_state))
153 .with_callback(
154 EventFilter::Component(ComponentEventFilter::AfterMount),
155 dataset,
156 Callback::from_ptr(mic_on_after_mount),
157 )
158 }
159}
160
161extern "C" fn mic_on_after_mount(mut data: RefAny, mut info: CallbackInfo) -> Update {
163 let (rate, channels) = {
164 let Some(mut s) = data.downcast_mut::<MicrophoneWidgetState>() else {
165 return Update::DoNothing;
166 };
167 if s.started {
168 return Update::DoNothing;
169 }
170 s.started = true;
171 let rate = if s.config.sample_rate > 0 {
172 s.config.sample_rate
173 } else {
174 48_000
175 };
176 let channels = s.config.channels.max(1);
177 (rate, channels)
178 };
179
180 info.add_thread(
181 ThreadId::unique(),
182 Thread::create(
183 RefAny::new(MicThreadInit {
184 sample_rate: rate,
185 channels,
186 }),
187 data.clone(),
188 ThreadCallback::new(mic_worker),
189 ),
190 );
191 Update::DoNothing
192}
193
194#[allow(clippy::cast_precision_loss)] extern "C" fn mic_worker(mut init: RefAny, mut sender: ThreadSender, _recv: ThreadReceiver) {
199 let (rate, channels) = init
200 .downcast_ref::<MicThreadInit>()
201 .map_or((48_000, 1), |i| (i.sample_rate, i.channels));
202
203 if let Some(backend) = mic_backend() {
206 let handle = (backend.open)(rate, channels);
207 if handle != 0 {
208 let mut buf: Vec<f32> = Vec::new();
209 loop {
210 let frames = (backend.read)(handle, &mut buf);
211 if frames == 0 {
212 break;
213 }
214 let frame = AudioFrame {
215 sample_rate: rate,
216 channels,
217 samples: F32Vec::from_vec(buf.clone()),
218 };
219 if !sender.send(ThreadReceiveMsg::WriteBack(ThreadWriteBackMsg::new(
220 WriteBackCallback::new(mic_writeback),
221 RefAny::new(frame),
222 ))) {
223 break;
224 }
225 }
226 (backend.close)(handle);
227 return;
228 }
229 }
230
231 let frames_per_chunk = (rate as usize / 50).max(1); let step = 2.0 * core::f32::consts::PI * 440.0 / rate as f32;
233 let mut phase: f32 = 0.0;
234 loop {
235 let mut samples = Vec::with_capacity(frames_per_chunk * channels as usize);
236 for _ in 0..frames_per_chunk {
237 let s = phase.sin() * 0.2;
238 phase += step;
239 if phase > 2.0 * core::f32::consts::PI {
240 phase -= 2.0 * core::f32::consts::PI;
241 }
242 for _ in 0..channels {
243 samples.push(s);
244 }
245 }
246 let frame = AudioFrame {
247 sample_rate: rate,
248 channels,
249 samples: F32Vec::from_vec(samples),
250 };
251 let sent = sender.send(ThreadReceiveMsg::WriteBack(ThreadWriteBackMsg::new(
252 WriteBackCallback::new(mic_writeback),
253 RefAny::new(frame),
254 )));
255 if !sent {
256 break;
257 }
258 std::thread::sleep(std::time::Duration::from_millis(20));
259 }
260}
261
262extern "C" fn mic_writeback(
265 mut writeback_data: RefAny,
266 mut frame_data: RefAny,
267 info: CallbackInfo,
268) -> Update {
269 let hook = match writeback_data.downcast_ref::<MicrophoneWidgetState>() {
270 Some(s) => s.on_frame.clone(),
271 None => return Update::DoNothing,
272 };
273 frame_data.downcast_ref::<AudioFrame>().map_or(Update::DoNothing, |frame| invoke_on_audio_frame(&hook, &info, frame.clone()))
274}
275
276extern "C" fn merge_microphone_state(mut new_data: RefAny, mut old_data: RefAny) -> RefAny {
279 {
280 let new_guard = new_data.downcast_mut::<MicrophoneWidgetState>();
281 let old_guard = old_data.downcast_ref::<MicrophoneWidgetState>();
282 if let (Some(mut new_g), Some(old_g)) = (new_guard, old_guard) {
283 new_g.started = old_g.started;
284 }
285 }
286 new_data
287}
288
289#[cfg(test)]
294#[allow(clippy::too_many_lines, clippy::cast_possible_truncation)]
295mod autotest_generated {
296 use std::{
297 collections::BTreeMap,
298 sync::{
299 mpsc::{channel, Receiver, Sender},
300 Arc, Mutex,
301 },
302 };
303
304 use azul_core::{
305 dom::{DomId, DomNodeId, NodeType},
306 geom::OptionLogicalPosition,
307 gl::OptionGlContextPtr,
308 hit_test::ScrollPosition,
309 resources::RendererResources,
310 styled_dom::NodeHierarchyItemId,
311 task::{
312 OptionThreadSendMsg, ThreadReceiverDestructorCallback, ThreadReceiverInner,
313 ThreadRecvCallback, ThreadSendMsg,
314 },
315 window::{MonitorVec, RawWindowHandle},
316 };
317 use azul_css::system::SystemStyle;
318 use rust_fontconfig::FcFontCache;
319
320 use super::*;
321 #[cfg(feature = "icu")]
322 use crate::icu::IcuLocalizerHandle;
323 use crate::{
324 callbacks::{CallbackChange, CallbackInfoRefData, ExternalSystemCallbacks},
325 thread::{
326 ThreadSendCallback, ThreadSenderDestructorCallback, ThreadSenderInner,
327 WriteBackCallbackType,
328 },
329 window::LayoutWindow,
330 window_state::FullWindowState,
331 };
332
333 const fn cfg(sample_rate: u32, channels: u16) -> AudioConfig {
339 AudioConfig {
340 sample_rate,
341 channels,
342 }
343 }
344
345 fn frame(sample_rate: u32, channels: u16, samples: Vec<f32>) -> AudioFrame {
348 AudioFrame {
349 sample_rate,
350 channels,
351 samples: F32Vec::from_vec(samples),
352 }
353 }
354
355 fn state(config: AudioConfig, started: bool) -> RefAny {
357 RefAny::new(MicrophoneWidgetState {
358 config,
359 started,
360 on_frame: OptionOnAudioFrame::None,
361 })
362 }
363
364 fn read_state(data: &mut RefAny) -> (AudioConfig, bool, bool) {
366 let s = data
367 .downcast_ref::<MicrophoneWidgetState>()
368 .expect("payload must still be a MicrophoneWidgetState");
369 (
370 s.config,
371 s.started,
372 matches!(s.on_frame, OptionOnAudioFrame::Some(_)),
373 )
374 }
375
376 struct FrameLog {
380 seen: Vec<(u32, u16, Vec<f32>)>,
381 }
382
383 extern "C" fn record_frame(mut data: RefAny, _: CallbackInfo, frame: AudioFrame) -> Update {
384 if let Some(mut log) = data.downcast_mut::<FrameLog>() {
385 log.seen.push((
386 frame.sample_rate,
387 frame.channels,
388 frame.samples.as_ref().to_vec(),
389 ));
390 }
391 Update::RefreshDom
392 }
393
394 extern "C" fn frame_do_nothing(_: RefAny, _: CallbackInfo, _: AudioFrame) -> Update {
395 Update::DoNothing
396 }
397
398 fn logged_frames(data: &mut RefAny) -> Vec<(u32, u16, Vec<f32>)> {
400 data.downcast_ref::<FrameLog>()
401 .expect("payload must still be a FrameLog")
402 .seen
403 .clone()
404 }
405
406 fn new_log() -> RefAny {
407 RefAny::new(FrameLog { seen: Vec::new() })
408 }
409
410 fn hook_into(log: &RefAny) -> OptionOnAudioFrame {
412 Some(OnAudioFrame {
413 refany: log.clone(),
414 callback: (record_frame as OnAudioFrameCallbackType).into(),
415 })
416 .into()
417 }
418
419 fn state_with_hook(config: AudioConfig, started: bool, log: &RefAny) -> RefAny {
421 RefAny::new(MicrophoneWidgetState {
422 config,
423 started,
424 on_frame: hook_into(log),
425 })
426 }
427
428 fn with_callback_info<R>(f: impl FnOnce(CallbackInfo) -> R) -> (R, Vec<CallbackChange>) {
434 let layout_window =
435 LayoutWindow::new(FcFontCache::default()).expect("LayoutWindow::new failed");
436 let renderer_resources = RendererResources::default();
437 let previous_window_state: Option<FullWindowState> = None;
438 let current_window_state = FullWindowState::default();
439 let gl_context = OptionGlContextPtr::None;
440 let scroll_states: BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>> =
441 BTreeMap::new();
442 let window_handle = RawWindowHandle::Unsupported;
443 let system_callbacks = ExternalSystemCallbacks::rust_internal();
444
445 let ref_data = CallbackInfoRefData {
446 layout_window: &layout_window,
447 renderer_resources: &renderer_resources,
448 previous_window_state: &previous_window_state,
449 current_window_state: ¤t_window_state,
450 gl_context: &gl_context,
451 current_scroll_manager: &scroll_states,
452 current_window_handle: &window_handle,
453 system_callbacks: &system_callbacks,
454 system_style: Arc::new(SystemStyle::default()),
455 monitors: Arc::new(Mutex::new(MonitorVec::from_const_slice(&[]))),
456 #[cfg(feature = "icu")]
457 icu_localizer: IcuLocalizerHandle::default(),
458 ctx: OptionRefAny::None,
459 };
460
461 let changes: Arc<Mutex<Vec<CallbackChange>>> = Arc::new(Mutex::new(Vec::new()));
462
463 let info = CallbackInfo::new(
464 &ref_data,
465 &changes,
466 DomNodeId {
467 dom: DomId::ROOT_ID,
468 node: NodeHierarchyItemId::NONE,
469 },
470 OptionLogicalPosition::None,
471 OptionLogicalPosition::None,
472 );
473
474 let out = f(info);
475 let recorded = core::mem::take(&mut *changes.lock().expect("change log poisoned"));
476 (out, recorded)
477 }
478
479 #[derive(Debug, Clone, PartialEq)]
483 struct SentFrame {
484 sample_rate: u32,
485 channels: u16,
486 samples: Vec<f32>,
487 writeback: usize,
489 }
490
491 static WORKER_LOG: Mutex<Vec<SentFrame>> = Mutex::new(Vec::new());
494 static WORKER_GATE: Mutex<()> = Mutex::new(());
495
496 extern "C" fn record_and_stop(
500 _sender: *const core::ffi::c_void,
501 msg: ThreadReceiveMsg,
502 ) -> bool {
503 if let ThreadReceiveMsg::WriteBack(mut wb) = msg {
504 let writeback = wb.callback.cb as usize;
505 if let Some(f) = wb.refany.downcast_ref::<AudioFrame>() {
506 WORKER_LOG
507 .lock()
508 .unwrap_or_else(std::sync::PoisonError::into_inner)
509 .push(SentFrame {
510 sample_rate: f.sample_rate,
511 channels: f.channels,
512 samples: f.samples.as_ref().to_vec(),
513 writeback,
514 });
515 }
516 }
517 false
518 }
519
520 extern "C" fn sender_drop_noop(_: *mut ThreadSenderInner) {}
521 extern "C" fn receiver_drop_noop(_: *mut ThreadReceiverInner) {}
522 extern "C" fn recv_nothing(_: *const core::ffi::c_void) -> OptionThreadSendMsg {
523 OptionThreadSendMsg::None
524 }
525
526 fn stopped_sender() -> (Receiver<ThreadReceiveMsg>, ThreadSender) {
528 let (tx, rx) = channel::<ThreadReceiveMsg>();
529 let sender = ThreadSender::new(ThreadSenderInner {
530 ptr: Box::new(tx),
531 send_fn: ThreadSendCallback { cb: record_and_stop },
532 destructor: ThreadSenderDestructorCallback {
533 cb: sender_drop_noop,
534 },
535 });
536 (rx, sender)
537 }
538
539 fn silent_receiver() -> (Sender<ThreadSendMsg>, ThreadReceiver) {
541 let (tx, rx) = channel::<ThreadSendMsg>();
542 let receiver = ThreadReceiver::new(ThreadReceiverInner {
543 ptr: Box::new(rx),
544 recv_fn: ThreadRecvCallback { cb: recv_nothing },
545 destructor: ThreadReceiverDestructorCallback {
546 cb: receiver_drop_noop,
547 },
548 });
549 (tx, receiver)
550 }
551
552 fn run_worker(init: RefAny) -> (Vec<SentFrame>, bool) {
561 let _gate = WORKER_GATE
562 .lock()
563 .unwrap_or_else(std::sync::PoisonError::into_inner);
564 WORKER_LOG
565 .lock()
566 .unwrap_or_else(std::sync::PoisonError::into_inner)
567 .clear();
568
569 let (_rx, sender) = stopped_sender();
570 let (_tx, receiver) = silent_receiver();
571 mic_worker(init, sender, receiver);
572
573 let sent = WORKER_LOG
574 .lock()
575 .unwrap_or_else(std::sync::PoisonError::into_inner)
576 .clone();
577 (sent, mic_backend().is_none())
578 }
579
580 fn expected_chunk_len(sample_rate: u32, channels: u16) -> usize {
583 (sample_rate as usize / 50).max(1) * channels as usize
584 }
585
586 #[test]
591 fn invoke_without_a_hook_is_donothing_even_for_a_degenerate_frame() {
592 let (update, _) = with_callback_info(|info| {
593 invoke_on_audio_frame(
596 &OptionOnAudioFrame::None,
597 &info,
598 frame(u32::MAX, 0, Vec::new()),
599 )
600 });
601 assert_eq!(update, Update::DoNothing);
602 }
603
604 #[test]
605 fn invoke_forwards_the_frame_verbatim_and_returns_the_hooks_update() {
606 let mut log = new_log();
607 let hook = hook_into(&log);
608 let samples = vec![-1.0_f32, 0.0, 1.0, 0.5];
609
610 let (update, _) = with_callback_info(|info| {
611 invoke_on_audio_frame(&hook, &info, frame(44_100, 2, samples.clone()))
612 });
613
614 assert_eq!(update, Update::RefreshDom, "the hook's Update must win");
615 assert_eq!(logged_frames(&mut log), vec![(44_100, 2, samples)]);
616 }
617
618 #[test]
619 fn invoke_passes_nan_infinite_and_negative_zero_samples_through_untouched() {
620 let mut log = new_log();
623 let hook = hook_into(&log);
624 let hostile = vec![f32::NAN, f32::INFINITY, f32::NEG_INFINITY, -0.0, f32::MIN, f32::MAX];
625
626 let (update, _) =
627 with_callback_info(|info| invoke_on_audio_frame(&hook, &info, frame(0, 1, hostile)));
628
629 assert_eq!(update, Update::RefreshDom);
630 let seen = logged_frames(&mut log);
631 assert_eq!(seen.len(), 1);
632 let (rate, channels, samples) = &seen[0];
633 assert_eq!((*rate, *channels), (0, 1), "a 0 Hz frame is forwarded as-is");
634 assert!(samples[0].is_nan(), "NaN must not be normalised");
635 assert_eq!(samples[1], f32::INFINITY);
636 assert_eq!(samples[2], f32::NEG_INFINITY);
637 assert!(
638 samples[3] == 0.0 && samples[3].is_sign_negative(),
639 "-0.0 must keep its sign bit"
640 );
641 assert_eq!(samples[4], f32::MIN);
642 assert_eq!(samples[5], f32::MAX);
643 }
644
645 #[test]
646 fn invoke_forwards_a_frame_whose_sample_count_contradicts_its_channel_count() {
647 let mut log = new_log();
650 let hook = hook_into(&log);
651 let bogus = frame(u32::MAX, u16::MAX, vec![0.1, 0.2, 0.3]);
652 assert_eq!(bogus.frame_count(), 0);
653
654 let (update, _) = with_callback_info(|info| invoke_on_audio_frame(&hook, &info, bogus));
655
656 assert_eq!(update, Update::RefreshDom);
657 assert_eq!(logged_frames(&mut log), vec![(u32::MAX, u16::MAX, vec![0.1, 0.2, 0.3])]);
658 }
659
660 #[test]
665 fn create_stores_the_config_verbatim_and_leaves_the_hook_unset() {
666 for (rate, channels) in [
667 (0, 0),
668 (1, 1),
669 (48_000, 2),
670 (u32::MAX, u16::MAX),
671 (u32::MAX, 0),
672 (0, u16::MAX),
673 ] {
674 let widget = MicrophoneWidget::create(cfg(rate, channels));
675 assert_eq!(
676 widget.config,
677 cfg(rate, channels),
678 "create must not normalise the config"
679 );
680 assert!(
681 matches!(widget.on_frame, OptionOnAudioFrame::None),
682 "a fresh widget has no frame hook"
683 );
684 }
685
686 let default = MicrophoneWidget::create(AudioConfig::default());
687 assert_eq!(default.config, cfg(48_000, 1));
688 }
689
690 #[test]
691 fn with_on_frame_installs_the_hook_keeps_the_config_and_shares_the_user_data() {
692 let data = new_log();
693 let widget = MicrophoneWidget::create(cfg(u32::MAX, u16::MAX))
694 .with_on_frame(data.clone(), record_frame as OnAudioFrameCallbackType);
695
696 assert_eq!(
697 widget.config,
698 cfg(u32::MAX, u16::MAX),
699 "the builder must not touch the config"
700 );
701 let OptionOnAudioFrame::Some(hook) = &widget.on_frame else {
702 panic!("with_on_frame must install a hook");
703 };
704 assert_eq!(
705 hook.callback.cb as usize,
706 record_frame as OnAudioFrameCallbackType as usize
707 );
708 assert_eq!(
709 hook.refany, data,
710 "the widget must hold the caller's RefAny, not a fresh allocation"
711 );
712 }
713
714 #[test]
715 fn set_on_frame_twice_keeps_only_the_last_hook_and_releases_the_first_payload() {
716 let first = new_log();
717 let second = new_log();
718 let mut widget = MicrophoneWidget::create(cfg(8_000, 1));
719 widget.set_on_frame(first.clone(), record_frame as OnAudioFrameCallbackType);
720 widget.set_on_frame(second.clone(), frame_do_nothing as OnAudioFrameCallbackType);
721
722 let OptionOnAudioFrame::Some(hook) = &widget.on_frame else {
723 panic!("hook must still be set");
724 };
725 assert_eq!(
726 hook.callback.cb as usize,
727 frame_do_nothing as OnAudioFrameCallbackType as usize,
728 "the second set_on_frame must replace the first"
729 );
730 assert_eq!(hook.refany, second);
731 assert_ne!(hook.refany, first, "the first payload must have been dropped");
732 assert_eq!(widget.config, cfg(8_000, 1));
733 }
734
735 #[test]
736 fn set_on_frame_accepts_the_same_refany_for_both_hooks() {
737 let mut data = new_log();
740 let mut widget = MicrophoneWidget::create(cfg(48_000, 2));
741 widget.set_on_frame(data.clone(), record_frame as OnAudioFrameCallbackType);
742 widget.set_on_frame(data.clone(), record_frame as OnAudioFrameCallbackType);
743
744 assert!(matches!(widget.on_frame, OptionOnAudioFrame::Some(_)));
745 assert!(logged_frames(&mut data).is_empty());
746 }
747
748 #[test]
753 fn dom_is_one_div_with_one_after_mount_callback_a_dataset_and_a_merge_callback() {
754 let dom = MicrophoneWidget::create(cfg(48_000, 2)).dom();
755
756 assert_eq!(dom.root.get_node_type(), &NodeType::Div);
757 assert_eq!(dom.children.as_ref().len(), 0, "the widget is a single node");
758
759 let callbacks = dom.root.get_callbacks();
760 assert_eq!(
761 callbacks.as_ref().len(),
762 1,
763 "exactly one callback: the AfterMount capture-thread starter"
764 );
765 assert_eq!(
766 callbacks.as_ref()[0].event,
767 EventFilter::Component(ComponentEventFilter::AfterMount)
768 );
769 assert_eq!(
770 callbacks.as_ref()[0].callback.cb,
771 mic_on_after_mount as CallbackType as usize
772 );
773
774 let merge = dom
775 .root
776 .get_merge_callback()
777 .expect("state must survive relayout");
778 assert_eq!(
779 merge.cb as usize,
780 merge_microphone_state as DatasetMergeCallbackType as usize
781 );
782
783 let mut dataset = dom
784 .root
785 .get_dataset()
786 .cloned()
787 .expect("the node must carry its MicrophoneWidgetState");
788 assert_eq!(read_state(&mut dataset), (cfg(48_000, 2), false, false));
789 }
790
791 #[test]
792 fn dom_shares_one_state_between_the_dataset_and_the_after_mount_callback() {
793 let dom = MicrophoneWidget::create(cfg(48_000, 1)).dom();
794 let mut dataset = dom.root.get_dataset().cloned().expect("dataset");
795 let mut callback_data = dom.root.get_callbacks().as_ref()[0].refany.clone();
796
797 assert_eq!(
798 callback_data, dataset,
799 "AfterMount must see the very state the dataset carries"
800 );
801
802 {
803 let mut s = dataset
804 .downcast_mut::<MicrophoneWidgetState>()
805 .expect("state");
806 s.started = true;
807 }
808 assert!(
809 read_state(&mut callback_data).1,
810 "a write through the dataset must be visible to the callback"
811 );
812 }
813
814 #[test]
815 fn dom_carries_an_extreme_config_and_the_hook_into_the_state_unnormalised() {
816 for (rate, channels) in [(0, 0), (1, u16::MAX), (u32::MAX, 1)] {
819 let dom = MicrophoneWidget::create(cfg(rate, channels))
820 .with_on_frame(new_log(), record_frame as OnAudioFrameCallbackType)
821 .dom();
822 let mut dataset = dom.root.get_dataset().cloned().expect("dataset");
823 assert_eq!(read_state(&mut dataset), (cfg(rate, channels), false, true));
824 }
825 }
826
827 #[test]
828 fn dom_built_twice_yields_two_independent_states() {
829 let a = MicrophoneWidget::create(cfg(8_000, 1)).dom();
830 let b = MicrophoneWidget::create(cfg(8_000, 1)).dom();
831 let mut a_ds = a.root.get_dataset().cloned().expect("dataset");
832 let mut b_ds = b.root.get_dataset().cloned().expect("dataset");
833
834 assert_ne!(a_ds, b_ds, "two widgets must not share one capture state");
835 {
836 let mut s = a_ds.downcast_mut::<MicrophoneWidgetState>().expect("state");
837 s.started = true;
838 }
839 assert!(!read_state(&mut b_ds).1, "the second widget is untouched");
840 }
841
842 #[test]
853 fn after_mount_ignores_a_dataset_that_is_not_a_microphone_state() {
854 let (update, changes) =
855 with_callback_info(|info| mic_on_after_mount(RefAny::new(0_u32), info));
856
857 assert_eq!(update, Update::DoNothing);
858 assert!(
859 changes.is_empty(),
860 "a foreign dataset must not start a capture thread"
861 );
862 }
863
864 #[test]
865 fn after_mount_is_a_no_op_once_the_capture_thread_has_started() {
866 let mut data = state(cfg(0, 0), true);
867 let (update, changes) = with_callback_info(|info| mic_on_after_mount(data.clone(), info));
868
869 assert_eq!(update, Update::DoNothing);
870 assert!(
871 changes.is_empty(),
872 "AfterMount must start the capture thread at most once"
873 );
874 assert_eq!(
875 read_state(&mut data),
876 (cfg(0, 0), true, false),
877 "a re-mount must not rewrite the state"
878 );
879 }
880
881 #[test]
882 fn after_mount_starts_nothing_while_the_state_is_borrowed_elsewhere() {
883 let data = state(cfg(48_000, 2), false);
886 let mut probe = data.clone();
887 let guard = probe
888 .downcast_ref::<MicrophoneWidgetState>()
889 .expect("shared borrow");
890
891 let (update, changes) = with_callback_info(|info| mic_on_after_mount(data.clone(), info));
892
893 assert_eq!(update, Update::DoNothing);
894 assert!(changes.is_empty(), "a borrowed state must not be mounted");
895 assert!(!guard.started, "the state must still be untouched");
896 drop(guard);
897
898 let mut after = data;
899 assert_eq!(read_state(&mut after), (cfg(48_000, 2), false, false));
900 }
901
902 #[test]
907 fn worker_stops_after_the_first_rejected_send_and_tags_frames_with_its_init() {
908 let (sent, tone_path) = run_worker(RefAny::new(MicThreadInit {
909 sample_rate: 8_000,
910 channels: 2,
911 }));
912
913 assert!(
916 sent.len() <= 1,
917 "the worker must stop after the first rejected send, not spin"
918 );
919 for f in &sent {
920 assert_eq!((f.sample_rate, f.channels), (8_000, 2));
921 assert_eq!(f.writeback, mic_writeback as WriteBackCallbackType as usize);
922 }
923
924 if !tone_path {
925 return; }
927 assert_eq!(sent.len(), 1);
928 let samples = &sent[0].samples;
929 assert_eq!(
930 samples.len(),
931 expected_chunk_len(8_000, 2),
932 "~20 ms of interleaved stereo at 8 kHz"
933 );
934 assert!(
935 samples.iter().all(|s| s.is_finite() && s.abs() <= 0.2),
936 "the tone must stay finite and inside +/-0.2"
937 );
938 assert_eq!(samples[0], 0.0, "the tone starts at phase 0");
939 for pair in samples.chunks_exact(2) {
940 assert_eq!(pair[0], pair[1], "both channels carry the same mono tone");
941 }
942 }
943
944 #[test]
945 fn worker_with_a_foreign_init_falls_back_to_48khz_mono() {
946 let (sent, tone_path) = run_worker(RefAny::new(0_u64));
947
948 for f in &sent {
949 assert_eq!(
950 (f.sample_rate, f.channels),
951 (48_000, 1),
952 "a bad init must not panic - it defaults"
953 );
954 }
955 if !tone_path {
956 return;
957 }
958 assert_eq!(sent.len(), 1);
959 assert_eq!(sent[0].samples.len(), expected_chunk_len(48_000, 1));
960 }
961
962 #[test]
963 fn worker_with_a_zero_sample_rate_emits_one_finite_chunk_instead_of_dividing_by_zero() {
964 let (sent, tone_path) = run_worker(RefAny::new(MicThreadInit {
968 sample_rate: 0,
969 channels: 1,
970 }));
971
972 for f in &sent {
975 assert_eq!((f.sample_rate, f.channels), (0, 1));
976 }
977 if !tone_path {
978 return;
979 }
980 assert!(
991 sent.iter().all(|f| f.samples.iter().all(|s| s.is_finite())),
992 "an infinite phase step must not leak NaN/inf into the samples"
993 );
994 assert_eq!(sent.len(), 1);
995 assert_eq!(sent[0].samples, vec![0.0_f32], "one frame, at phase 0");
996 }
997
998 #[test]
999 fn worker_with_zero_channels_emits_an_empty_chunk_and_stops() {
1000 let (sent, tone_path) = run_worker(RefAny::new(MicThreadInit {
1001 sample_rate: 48_000,
1002 channels: 0,
1003 }));
1004
1005 for f in &sent {
1006 assert_eq!(f.channels, 0);
1007 }
1008 if !tone_path {
1009 return;
1010 }
1011 assert_eq!(sent.len(), 1);
1012 assert!(
1013 sent[0].samples.is_empty(),
1014 "0 channels interleaves 0 samples per frame"
1015 );
1016 assert_eq!(frame(48_000, 0, sent[0].samples.clone()).frame_count(), 0);
1018 }
1019
1020 #[test]
1021 fn worker_chunk_length_clamps_to_one_frame_for_sub_50hz_rates() {
1022 for (rate, channels, expected) in [
1025 (1_u32, 1_u16, 1_usize),
1026 (49, 1, 1),
1027 (50, 1, 1),
1028 (99, 2, 2),
1029 (100, 2, 4),
1030 (100, 3, 6),
1031 ] {
1032 let (sent, tone_path) = run_worker(RefAny::new(MicThreadInit {
1033 sample_rate: rate,
1034 channels,
1035 }));
1036 if !tone_path {
1037 return;
1038 }
1039 assert_eq!(sent.len(), 1, "rate {rate} must emit exactly one chunk");
1040 assert_eq!(
1041 sent[0].samples.len(),
1042 expected,
1043 "rate {rate} x {channels} ch must clamp to >= 1 frame"
1044 );
1045 assert_eq!(expected_chunk_len(rate, channels), expected);
1046 assert!(
1047 sent[0].samples.iter().all(|s| s.is_finite() && s.abs() <= 0.2),
1048 "a phase step larger than a full period must still yield bounded samples"
1049 );
1050 }
1051 }
1052
1053 #[test]
1058 fn writeback_hands_the_frame_to_the_hook_and_returns_its_update() {
1059 let mut log = new_log();
1060 let data = state_with_hook(cfg(44_100, 2), true, &log);
1061 let frame_data = RefAny::new(frame(44_100, 2, vec![0.25, -0.25, 0.5, -0.5]));
1062
1063 let (update, _) =
1064 with_callback_info(|info| mic_writeback(data.clone(), frame_data.clone(), info));
1065
1066 assert_eq!(update, Update::RefreshDom, "the hook's Update must win");
1067 assert_eq!(
1068 logged_frames(&mut log),
1069 vec![(44_100, 2, vec![0.25, -0.25, 0.5, -0.5])]
1070 );
1071 }
1072
1073 #[test]
1074 fn writeback_without_a_hook_is_a_no_op() {
1075 let data = state(cfg(48_000, 1), true);
1076 let frame_data = RefAny::new(frame(48_000, 1, vec![0.0; 8]));
1077
1078 let (update, changes) =
1079 with_callback_info(|info| mic_writeback(data.clone(), frame_data.clone(), info));
1080
1081 assert_eq!(update, Update::DoNothing);
1082 assert!(changes.is_empty(), "audio has no texture - nothing to change");
1083 }
1084
1085 #[test]
1086 fn writeback_ignores_frame_data_of_the_wrong_type() {
1087 let mut log = new_log();
1088 let data = state_with_hook(cfg(48_000, 1), true, &log);
1089
1090 let (update, changes) =
1091 with_callback_info(|info| mic_writeback(data.clone(), RefAny::new(0_u32), info));
1092
1093 assert_eq!(update, Update::DoNothing);
1094 assert!(changes.is_empty());
1095 assert!(
1096 logged_frames(&mut log).is_empty(),
1097 "the user hook must not fire without a frame"
1098 );
1099 }
1100
1101 #[test]
1102 fn writeback_survives_a_writeback_dataset_that_is_not_a_microphone_state() {
1103 let (update, changes) = with_callback_info(|info| {
1104 mic_writeback(RefAny::new(0_u32), RefAny::new(frame(8_000, 1, vec![0.0])), info)
1105 });
1106
1107 assert_eq!(
1108 update,
1109 Update::DoNothing,
1110 "a foreign dataset means no hook - but no panic either"
1111 );
1112 assert!(changes.is_empty());
1113 }
1114
1115 #[test]
1116 fn writeback_forwards_a_malformed_frame_to_the_hook_untouched() {
1117 let mut log = new_log();
1121 let data = state_with_hook(cfg(48_000, 2), true, &log);
1122 let bogus = RefAny::new(frame(u32::MAX, u16::MAX, Vec::new()));
1123
1124 let (update, _) =
1125 with_callback_info(|info| mic_writeback(data.clone(), bogus.clone(), info));
1126
1127 assert_eq!(update, Update::RefreshDom);
1128 assert_eq!(logged_frames(&mut log), vec![(u32::MAX, u16::MAX, Vec::new())]);
1129 }
1130
1131 #[test]
1132 fn writeback_is_a_no_op_while_the_state_is_mutably_borrowed() {
1133 let mut log = new_log();
1134 let data = state_with_hook(cfg(48_000, 1), true, &log);
1135 let mut probe = data.clone();
1136 let guard = probe
1137 .downcast_mut::<MicrophoneWidgetState>()
1138 .expect("exclusive borrow");
1139
1140 let frame_data = RefAny::new(frame(48_000, 1, vec![0.1]));
1141 let (update, changes) =
1142 with_callback_info(|info| mic_writeback(data.clone(), frame_data.clone(), info));
1143
1144 assert_eq!(update, Update::DoNothing, "a blocked downcast must not panic");
1145 assert!(changes.is_empty());
1146 drop(guard);
1147 assert!(logged_frames(&mut log).is_empty());
1148 }
1149
1150 #[test]
1155 fn merge_takes_started_from_old_and_everything_else_from_new() {
1156 let log = new_log();
1157 let new_data = state_with_hook(cfg(44_100, 2), false, &log);
1158 let old_data = state(cfg(8_000, 1), true);
1159
1160 let mut merged = merge_microphone_state(new_data, old_data);
1161
1162 assert_eq!(
1163 read_state(&mut merged),
1164 (cfg(44_100, 2), true, true),
1165 "config + hook come from the fresh build, 'started' from the old state"
1166 );
1167 }
1168
1169 #[test]
1170 fn merge_takes_started_from_old_even_when_that_clears_it() {
1171 let new_data = state(cfg(48_000, 1), true);
1174 let old_data = state(cfg(48_000, 1), false);
1175
1176 let mut merged = merge_microphone_state(new_data, old_data);
1177 assert!(!read_state(&mut merged).1);
1178 }
1179
1180 #[test]
1181 fn merge_returns_the_new_allocation_itself_not_a_copy() {
1182 let new_data = state(cfg(48_000, 1), false);
1183 let handle = new_data.clone();
1184
1185 let merged = merge_microphone_state(new_data, state(cfg(48_000, 1), true));
1186
1187 assert_eq!(merged, handle, "merge must hand back the same state object");
1188 }
1189
1190 #[test]
1191 fn merge_leaves_the_new_state_alone_when_the_old_one_is_foreign() {
1192 let new_data = state(cfg(48_000, 2), true);
1193 let mut merged = merge_microphone_state(new_data, RefAny::new(0_u32));
1194
1195 assert_eq!(
1196 read_state(&mut merged),
1197 (cfg(48_000, 2), true, false),
1198 "nothing to carry forward from a foreign payload"
1199 );
1200 }
1201
1202 #[test]
1203 fn merge_returns_a_foreign_new_dataset_untouched() {
1204 let old_data = state(cfg(48_000, 1), true);
1205 let mut merged = merge_microphone_state(RefAny::new(77_u32), old_data);
1206
1207 assert_eq!(
1208 merged.downcast_ref::<u32>().map(|v| *v),
1209 Some(77),
1210 "merge must hand back exactly the payload it was given"
1211 );
1212 }
1213
1214 #[test]
1215 fn merge_of_a_dataset_with_itself_does_not_panic() {
1216 let mut data = state_with_hook(cfg(48_000, 2), true, &new_log());
1219 let mut merged = merge_microphone_state(data.clone(), data.clone());
1220
1221 assert_eq!(read_state(&mut merged), (cfg(48_000, 2), true, true));
1222 assert_eq!(read_state(&mut data), (cfg(48_000, 2), true, true));
1223 }
1224
1225 #[test]
1226 fn a_rebuilt_dom_merges_the_running_thread_flag_forward() {
1227 let old = MicrophoneWidget::create(cfg(48_000, 1)).dom();
1231 let mut old_ds = old.root.get_dataset().cloned().expect("dataset");
1232 {
1233 let mut s = old_ds
1234 .downcast_mut::<MicrophoneWidgetState>()
1235 .expect("state");
1236 s.started = true;
1237 }
1238
1239 let new = MicrophoneWidget::create(cfg(44_100, 2))
1240 .with_on_frame(new_log(), record_frame as OnAudioFrameCallbackType)
1241 .dom();
1242 let new_ds = new.root.get_dataset().cloned().expect("dataset");
1243 let merge = new.root.get_merge_callback().expect("merge callback");
1244
1245 let mut merged = (merge.cb)(new_ds, old_ds);
1246
1247 assert_eq!(
1248 read_state(&mut merged),
1249 (cfg(44_100, 2), true, true),
1250 "the rebuilt widget keeps its new config + hook but inherits the thread"
1251 );
1252 }
1253}