Skip to main content

android_media/
mic.rs

1use {
2    super::{constants::*, load_class},
3    jni::{
4        JNIEnv,
5        errors::Error as JniError,
6        objects::{GlobalRef, JByteArray, JObject, JString},
7    },
8    std::{
9        collections::HashMap,
10        error::Error,
11        fmt::{Display, Formatter, Result as FmtResult},
12        mem::transmute,
13        sync::{
14            LazyLock, Mutex, PoisonError,
15            atomic::{AtomicI16, Ordering},
16        },
17    },
18    tokio::sync::oneshot::{Sender, channel, error::RecvError},
19    tracing::error,
20};
21
22#[derive(Debug)]
23pub enum MicError {
24    Jni(JniError),
25    Poison(String),
26    Recv(RecvError),
27    Read(String),
28}
29
30impl Display for MicError {
31    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
32        write!(f, "MicError: ")?;
33        match self {
34            Self::Jni(e) => Display::fmt(e, f),
35            Self::Poison(msg) => write!(f, "PoisonError: {}", msg),
36            Self::Recv(e) => Display::fmt(e, f),
37            Self::Read(msg) => write!(f, "ReadError: {}", msg),
38        }
39    }
40}
41
42impl Error for MicError {}
43
44impl From<JniError> for MicError {
45    fn from(value: JniError) -> Self {
46        Self::Jni(value)
47    }
48}
49
50impl From<RecvError> for MicError {
51    fn from(value: RecvError) -> Self {
52        Self::Recv(value)
53    }
54}
55
56impl<T> From<PoisonError<T>> for MicError {
57    fn from(value: PoisonError<T>) -> Self {
58        Self::Poison(value.to_string())
59    }
60}
61
62type MicResult<T> = Result<T, MicError>;
63
64/// Microphone read task callbacks
65static MIC_READ_CALLBACKS: LazyLock<Mutex<HashMap<i16, Sender<MicResult<Vec<u8>>>>>> =
66    LazyLock::new(|| Mutex::new(Default::default()));
67
68/// Rust wrapper for Android microphone service
69pub struct AudioMicrophone {
70    /// JNI environment
71    env: JNIEnv<'static>,
72    /// Java object instance
73    this: GlobalRef,
74    /// Sample rate (Hz)
75    sample_rate: i32,
76    /// Audio channel count
77    channel_count: i32,
78    /// Audio encoding format
79    audio_format: i32,
80}
81
82//noinspection SpellCheckingInspection
83impl AudioMicrophone {
84    /// Create a new AudioMicrophone instance
85    ///
86    /// # Arguments
87    /// * `env` - JNI environment
88    /// * `context` - GlobalRef to context object (used to get the correct ClassLoader)
89    /// * `sample_rate` - Sample rate (e.g., 16000, 44100, 48000)
90    /// * `channel_config` - Channel configuration (use ChannelInConfig constants)
91    /// * `audio_format` - Audio encoding format (use AudioEncoding constants)
92    ///
93    /// # Returns
94    /// Returns initialized AudioMicrophone instance
95    ///
96    /// # Examples
97    ///
98    /// See the README.md for complete usage examples.
99    pub fn new(
100        mut env: JNIEnv,
101        context: &GlobalRef,
102        sample_rate: i32,
103        channel_config: i32,
104        audio_format: i32,
105    ) -> MicResult<Self> {
106        // Load Mic class using ClassLoader
107        let mic_class = load_class(&mut env, context, "rust.android.Mic")?;
108
109        // Create Mic Java object
110        let mic_obj = env.new_object(
111            &mic_class,
112            "(III)V",
113            &[
114                sample_rate.into(),
115                channel_config.into(),
116                audio_format.into(),
117            ],
118        )?;
119        let this = env.new_global_ref(mic_obj)?;
120
121        // Initialize audio recorder
122        env.call_method(
123            &this,
124            "init",
125            "(IIII)V",
126            &[
127                AudioSource::Mic.value().into(),
128                sample_rate.into(),
129                channel_config.into(),
130                audio_format.into(),
131            ],
132        )?;
133
134        unsafe {
135            Ok(Self {
136                env: transmute(env),
137                this,
138                sample_rate,
139                channel_count: if channel_config == ChannelInConfig::Mono.value() {
140                    1
141                } else {
142                    2
143                },
144                audio_format,
145            })
146        }
147    }
148
149    fn get_env(&self) -> JNIEnv<'_> {
150        unsafe { self.env.unsafe_clone() }
151    }
152
153    /// Start recording
154    pub fn start(&self) -> MicResult<()> {
155        self.get_env()
156            .call_method(&self.this, "startRecording", "()V", &[])?;
157
158        Ok(())
159    }
160
161    /// Stop recording
162    pub fn stop(&self) -> MicResult<()> {
163        self.get_env().call_method(&self.this, "stop", "()V", &[])?;
164        Ok(())
165    }
166
167    /// Release resources
168    pub fn release(&self) -> MicResult<()> {
169        self.get_env()
170            .call_method(&self.this, "release", "()V", &[])?;
171
172        Ok(())
173    }
174
175    /// Read audio data asynchronously
176    ///
177    /// # Arguments
178    /// * `duration_ms` - Duration of audio to read in milliseconds
179    ///
180    /// # Returns
181    /// * `Result<Vec<u8>, MicError>` - Audio data (raw PCM data)
182    ///
183    /// # Buffer Design
184    /// To prevent stuttering, the internal buffer size is automatically calculated as 2x the requested duration.
185    /// For example, if you want to read 1 second (1000ms) of data, the internal buffer will prepare at least 2 seconds of data.
186    ///
187    /// # Examples
188    ///
189    /// See README.md for complete usage examples.
190    /// ```
191    pub async fn read(&self, duration_ms: i32) -> MicResult<Vec<u8>> {
192        static ID: AtomicI16 = AtomicI16::new(0);
193        let task_id = ID.fetch_add(1, Ordering::AcqRel);
194        let (tx, rx) = channel();
195
196        {
197            let mut lock = MIC_READ_CALLBACKS.lock()?;
198            lock.insert(task_id, tx);
199        }
200
201        // Calculate number of bytes to read
202        // Formula: duration_ms / 1000 * sample_rate * channel_count * bytes_per_sample
203        let audio_encoding = match self.audio_format {
204            x if x == AudioEncoding::Pcm8bit.value() => AudioEncoding::Pcm8bit,
205            x if x == AudioEncoding::Pcm16bit.value() => AudioEncoding::Pcm16bit,
206            x if x == AudioEncoding::PcmFloat.value() => AudioEncoding::PcmFloat,
207            _ => AudioEncoding::Pcm16bit,
208        };
209        let bytes_per_sample = audio_encoding.bytes_per_sample();
210        let samples_per_channel = (duration_ms * self.sample_rate) / 1000;
211        let total_samples = samples_per_channel * self.channel_count;
212        let requested_bytes = total_samples * bytes_per_sample;
213
214        // Buffer design: use 2x requested duration as buffer size to prevent stuttering
215        let buffer_multiplier = 2;
216        let buffer_bytes = requested_bytes * buffer_multiplier;
217
218        // Call Java layer's read method
219        self.get_env()
220            .call_method(
221                &self.this,
222                "performRead",
223                "(SII)V",
224                &[task_id.into(), requested_bytes.into(), buffer_bytes.into()],
225            )?
226            .v()?;
227
228        rx.await?
229    }
230
231    /// Get sample rate
232    pub fn get_sample_rate(&self) -> i32 {
233        self.sample_rate
234    }
235
236    /// Get channel count
237    pub fn get_channel_count(&self) -> i32 {
238        self.channel_count
239    }
240
241    /// Get audio format
242    pub fn get_audio_format(&self) -> i32 {
243        self.audio_format
244    }
245
246    /// Get buffer size
247    pub fn get_buffer_size(&self) -> MicResult<i32> {
248        Ok(self
249            .get_env()
250            .call_method(&self.this, "getBufferSize", "()I", &[])?
251            .i()?)
252    }
253
254    /// Calculate number of bytes for a specified duration
255    pub fn calculate_bytes_for_duration(&self, duration_ms: i32) -> i32 {
256        let audio_encoding = match self.audio_format {
257            x if x == AudioEncoding::Pcm8bit.value() => AudioEncoding::Pcm8bit,
258            x if x == AudioEncoding::Pcm16bit.value() => AudioEncoding::Pcm16bit,
259            x if x == AudioEncoding::PcmFloat.value() => AudioEncoding::PcmFloat,
260            _ => AudioEncoding::Pcm16bit,
261        };
262        let bytes_per_sample = audio_encoding.bytes_per_sample();
263        let samples_per_channel = (duration_ms * self.sample_rate) / 1000;
264        samples_per_channel * self.channel_count * bytes_per_sample
265    }
266}
267
268unsafe impl Send for AudioMicrophone {}
269unsafe impl Sync for AudioMicrophone {}
270
271impl Drop for AudioMicrophone {
272    fn drop(&mut self) {
273        if let Err(e) = self.release() {
274            error!(?e, "Dropping error.");
275        }
276    }
277}
278
279//noinspection SpellCheckingInspection
280/// Microphone read success callback
281///
282/// This function is called by the Java layer after audio data is successfully read
283///
284/// # Arguments
285/// * `env` - JNI environment
286/// * `_this` - Java object
287/// * `task_id` - Task ID
288/// * `audio_data` - Audio data byte array
289#[allow(non_snake_case)]
290#[unsafe(no_mangle)]
291extern "C" fn Java_rust_android_Mic_onReadSuccess(
292    env: JNIEnv,
293    _this: JObject,
294    task_id: i16,
295    audio_data: JByteArray,
296) {
297    let Ok(mut lock) = MIC_READ_CALLBACKS.lock() else {
298        return;
299    };
300    let Some(tx) = lock.remove(&task_id) else {
301        return;
302    };
303    drop(lock);
304
305    if audio_data.is_null() {
306        let _ = tx.send(Err(MicError::Read("Read succeeded but audio data is empty".into())));
307        return;
308    }
309
310    let mut buf = vec![0; env.get_array_length(&audio_data).unwrap_or(0) as _];
311    let _ = match env.get_byte_array_region(&audio_data, 0, &mut buf) {
312        Ok(()) => tx.send(Ok(buf.into_iter().map(|i| i as _).collect::<Vec<_>>())),
313        Err(e) => tx.send(Err(e.into())),
314    };
315}
316
317//noinspection SpellCheckingInspection
318/// Microphone read failure callback
319///
320/// This function is called by the Java layer after audio data read fails
321///
322/// # Arguments
323/// * `env` - JNI environment
324/// * `_this` - Java object
325/// * `task_id` - Task ID
326/// * `error_message` - Error message (string)
327#[allow(non_snake_case)]
328#[unsafe(no_mangle)]
329extern "C" fn Java_rust_android_Mic_onReadFailure(
330    mut env: JNIEnv,
331    _this: JObject,
332    task_id: i16,
333    error_message: JString,
334) {
335    let Ok(mut lock) = MIC_READ_CALLBACKS.lock() else {
336        return;
337    };
338    let Some(tx) = lock.remove(&task_id) else {
339        return;
340    };
341
342    let error_msg = match env.get_string(&error_message) {
343        Ok(msg) => msg.to_string_lossy().to_string(),
344        Err(e) => format!("Parsing error failed: {}", e),
345    };
346    if let Err(e) = tx.send(Err(MicError::Read(format!(
347        "Mic read failed: {}",
348        error_msg
349    )))) {
350        error!(?e, "Sending error failed.");
351    }
352}