Skip to main content

azul_layout/widgets/
microphone.rs

1//! Microphone-capture widget (SUPER_PLAN_2 ยง4 P7) - a "dumb widget" with the
2//! same architecture as the camera/screencap/video widgets, only the medium is
3//! audio (no GL texture).
4//!
5//! `MicrophoneWidget::create(config).with_on_frame(data, cb).dom()` yields an
6//! invisible node that, on `AfterMount`, starts a background capture thread.
7//! Each captured [`AudioFrame`] flows through the writeback to the user's
8//! `on_frame` hook (the backreference DI pattern), so app code can save,
9//! process, or **send** the audio over the network (the azul-meet audio seam) -
10//! all via the public API, no globals. The mic permission is the existing
11//! `Capability::Microphone`.
12//!
13//! This tick uses a self-contained **test-tone** worker (a 440 Hz sine, no
14//! platform deps); the real AVAudioEngine / AAudio / cpal capture worker
15//! (dll-side) swaps in later.
16
17use 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; // for impl_widget_callback!'s impl_option!
27use azul_css::F32Vec;
28
29use crate::callbacks::{Callback, CallbackInfo, CallbackType};
30use crate::thread::{
31    Thread, ThreadCallback, ThreadReceiveMsg, ThreadSender, ThreadWriteBackMsg, WriteBackCallback,
32};
33
34// --- User hook: on_frame (backreference DI, FFI-exposed) ---
35
36/// User hook fired once per captured audio chunk - the backreference DI pattern
37/// (see `architecture.md`). The widget's private writeback invokes it with each
38/// [`AudioFrame`] so application code can save it, apply effects, or send it
39/// over the network (azul-meet). Returns `Update` like any callback. Wired via
40/// [`MicrophoneWidget::with_on_frame`].
41pub type OnAudioFrameCallbackType = extern "C" fn(RefAny, CallbackInfo, AudioFrame) -> Update;
42impl_widget_callback!(
43    OnAudioFrame,
44    OptionOnAudioFrame,
45    OnAudioFrameCallback,
46    OnAudioFrameCallbackType
47);
48
49// Host-invoker plumbing for managed-FFI bindings - see core/src/host_invoker.rs.
50azul_core::impl_managed_callback! {
51    wrapper:        OnAudioFrameCallback,
52    info_ty:        CallbackInfo,
53    return_ty:      Update,
54    default_ret:    Update::DoNothing,
55    invoker_static: ON_AUDIO_FRAME_INVOKER,
56    invoker_ty:     AzOnAudioFrameCallbackInvoker,
57    thunk_fn:       az_on_audio_frame_callback_thunk,
58    setter_fn:      AzApp_setOnAudioFrameCallbackInvoker,
59    from_handle_fn: AzOnAudioFrameCallback_createFromHostHandle,
60    extra_args:     [ frame: AudioFrame ],
61}
62
63/// Invoke the optional `on_frame` hook with `frame`, returning the user's
64/// `Update` (`DoNothing` when no hook is set).
65fn invoke_on_audio_frame(
66    hook: &OptionOnAudioFrame,
67    info: &mut CallbackInfo,
68    frame: AudioFrame,
69) -> Update {
70    match hook {
71        OptionOnAudioFrame::Some(h) => (h.callback.cb)(h.refany.clone(), info.clone(), frame),
72        OptionOnAudioFrame::None => Update::DoNothing,
73    }
74}
75
76/// Init data handed to the capture worker thread.
77struct MicThreadInit {
78    sample_rate: u32,
79    channels: u16,
80}
81
82/// Live state for one microphone widget, carried across relayout by
83/// [`merge_microphone_state`].
84pub struct MicrophoneWidgetState {
85    /// The requested capture configuration (rate + channels).
86    pub config: AudioConfig,
87    /// `true` once the capture thread has been started.
88    pub started: bool,
89    /// Optional user hook invoked with each captured frame (save / effects /
90    /// send). Re-set on every fresh build (see [`merge_microphone_state`]).
91    pub on_frame: OptionOnAudioFrame,
92}
93
94/// A microphone-capture widget. `create(config).with_on_frame(..).dom()` yields
95/// an invisible node a background capture thread feeds.
96#[repr(C)]
97pub struct MicrophoneWidget {
98    /// Requested capture config (sample rate, channels).
99    pub config: AudioConfig,
100    /// Optional per-frame user hook (save / effects / send - azul-meet).
101    pub on_frame: OptionOnAudioFrame,
102}
103
104impl MicrophoneWidget {
105    /// Create a microphone widget for the given capture config.
106    pub fn create(config: AudioConfig) -> Self {
107        Self {
108            config,
109            on_frame: OptionOnAudioFrame::None,
110        }
111    }
112
113    /// Set a hook invoked with every captured audio chunk - for saving,
114    /// effects, or sending over the network (azul-meet). The backreference DI
115    /// pattern (see `architecture.md`).
116    pub fn set_on_frame<C: Into<OnAudioFrameCallback>>(&mut self, data: RefAny, on_frame: C) {
117        self.on_frame = Some(OnAudioFrame {
118            refany: data,
119            callback: on_frame.into(),
120        })
121        .into();
122    }
123
124    /// Builder form of [`set_on_frame`](Self::set_on_frame).
125    pub fn with_on_frame<C: Into<OnAudioFrameCallback>>(
126        mut self,
127        data: RefAny,
128        on_frame: C,
129    ) -> Self {
130        self.set_on_frame(data, on_frame);
131        self
132    }
133
134    /// Build the widget's DOM: a single invisible node, fed by a background
135    /// capture thread started on mount. Place it anywhere in your tree - the
136    /// capture lives as long as the node is mounted (unmount stops it).
137    pub fn dom(self) -> Dom {
138        let state = MicrophoneWidgetState {
139            config: self.config,
140            started: false,
141            on_frame: self.on_frame,
142        };
143        let dataset = RefAny::new(state);
144
145        Dom::create_div()
146            .with_dataset(OptionRefAny::Some(dataset.clone()))
147            .with_merge_callback(merge_microphone_state as DatasetMergeCallbackType)
148            .with_callback(
149                EventFilter::Component(ComponentEventFilter::AfterMount),
150                dataset,
151                Callback::from(mic_on_after_mount as CallbackType),
152            )
153    }
154}
155
156/// AfterMount: start the background capture thread exactly once.
157extern "C" fn mic_on_after_mount(mut data: RefAny, mut info: CallbackInfo) -> Update {
158    let (rate, channels) = {
159        let mut s = match data.downcast_mut::<MicrophoneWidgetState>() {
160            Some(s) => s,
161            None => return Update::DoNothing,
162        };
163        if s.started {
164            return Update::DoNothing;
165        }
166        s.started = true;
167        let rate = if s.config.sample_rate > 0 {
168            s.config.sample_rate
169        } else {
170            48_000
171        };
172        let channels = s.config.channels.max(1);
173        (rate, channels)
174    };
175
176    info.add_thread(
177        ThreadId::unique(),
178        Thread::create(
179            RefAny::new(MicThreadInit {
180                sample_rate: rate,
181                channels,
182            }),
183            data.clone(),
184            ThreadCallback::new(mic_worker),
185        ),
186    );
187    Update::DoNothing
188}
189
190/// Background worker (test tone): a 440 Hz sine in ~20 ms chunks until the
191/// widget unmounts. The real AVAudioEngine / AAudio / cpal capture loop
192/// replaces it (dll-side).
193extern "C" fn mic_worker(mut init: RefAny, mut sender: ThreadSender, _recv: ThreadReceiver) {
194    let (rate, channels) = init
195        .downcast_ref::<MicThreadInit>()
196        .map(|i| (i.sample_rate, i.channels))
197        .unwrap_or((48_000, 1));
198
199    // Real platform capture if the dll registered a mic backend (ALSA on
200    // Linux); otherwise the 440 Hz test tone below.
201    if let Some(backend) = mic_backend() {
202        let handle = (backend.open)(rate, channels);
203        if handle != 0 {
204            let mut buf: Vec<f32> = Vec::new();
205            loop {
206                let frames = (backend.read)(handle, &mut buf);
207                if frames == 0 {
208                    break;
209                }
210                let frame = AudioFrame {
211                    sample_rate: rate,
212                    channels,
213                    samples: F32Vec::from_vec(buf.clone()),
214                };
215                if !sender.send(ThreadReceiveMsg::WriteBack(ThreadWriteBackMsg::new(
216                    WriteBackCallback::new(mic_writeback),
217                    RefAny::new(frame),
218                ))) {
219                    break;
220                }
221            }
222            (backend.close)(handle);
223            return;
224        }
225    }
226
227    let frames_per_chunk = (rate as usize / 50).max(1); // ~20 ms
228    let step = 2.0 * core::f32::consts::PI * 440.0 / rate as f32;
229    let mut phase: f32 = 0.0;
230    loop {
231        let mut samples = Vec::with_capacity(frames_per_chunk * channels as usize);
232        for _ in 0..frames_per_chunk {
233            let s = phase.sin() * 0.2;
234            phase += step;
235            if phase > 2.0 * core::f32::consts::PI {
236                phase -= 2.0 * core::f32::consts::PI;
237            }
238            for _ in 0..channels {
239                samples.push(s);
240            }
241        }
242        let frame = AudioFrame {
243            sample_rate: rate,
244            channels,
245            samples: F32Vec::from_vec(samples),
246        };
247        let sent = sender.send(ThreadReceiveMsg::WriteBack(ThreadWriteBackMsg::new(
248            WriteBackCallback::new(mic_writeback),
249            RefAny::new(frame),
250        )));
251        if !sent {
252            break;
253        }
254        std::thread::sleep(std::time::Duration::from_millis(20));
255    }
256}
257
258/// Writeback (main thread): hand the captured frame to the user's `on_frame`
259/// hook. No GL - audio has no texture.
260extern "C" fn mic_writeback(
261    mut writeback_data: RefAny,
262    mut frame_data: RefAny,
263    mut info: CallbackInfo,
264) -> Update {
265    let hook = match writeback_data.downcast_ref::<MicrophoneWidgetState>() {
266        Some(s) => s.on_frame.clone(),
267        None => return Update::DoNothing,
268    };
269    match frame_data.downcast_ref::<AudioFrame>() {
270        Some(frame) => invoke_on_audio_frame(&hook, &mut info, frame.clone()),
271        None => Update::DoNothing,
272    }
273}
274
275/// Carry live state forward across relayout (config + started; the on_frame
276/// hook is taken from the fresh build).
277extern "C" fn merge_microphone_state(mut new_data: RefAny, mut old_data: RefAny) -> RefAny {
278    {
279        let new_guard = new_data.downcast_mut::<MicrophoneWidgetState>();
280        let old_guard = old_data.downcast_ref::<MicrophoneWidgetState>();
281        if let (Some(mut new_g), Some(old_g)) = (new_guard, old_guard) {
282            new_g.started = old_g.started;
283        }
284    }
285    new_data
286}