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

            
17
use alloc::vec::Vec;
18

            
19
use azul_core::audio::{AudioConfig, AudioFrame};
20

            
21
use super::capture_common::mic_backend;
22
use azul_core::callbacks::Update;
23
use azul_core::dom::{ComponentEventFilter, DatasetMergeCallbackType, Dom, EventFilter};
24
use azul_core::refany::{OptionRefAny, RefAny};
25
use azul_core::task::{ThreadId, ThreadReceiver};
26
use azul_css::impl_option_inner; // for impl_widget_callback!'s impl_option!
27
use azul_css::F32Vec;
28

            
29
use crate::callbacks::{Callback, CallbackInfo, CallbackType};
30
use 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`].
41
pub type OnAudioFrameCallbackType = extern "C" fn(RefAny, CallbackInfo, AudioFrame) -> Update;
42
impl_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.
50
azul_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).
65
fn 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.
77
struct 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`].
84
pub 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)]
97
pub 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

            
104
impl 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.
157
extern "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).
193
extern "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.
260
extern "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).
277
extern "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
}