#![allow(unsafe_code)]
use std::collections::VecDeque;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::SyncSender;
use std::sync::{Arc, Mutex};
use std::thread::{self, JoinHandle};
use crate::audio::{AudioCapture, AudioCaptureConfig};
use crate::{CaptureError, Select};
use mediaway_common::{AudioFrame, Bytes, CodecKind, Rational, SampleFormat, StreamInfo};
use ndk::audio::{
AudioDirection, AudioFormat, AudioPerformanceMode, AudioSharingMode, AudioStream,
AudioStreamBuilder,
};
const PCM_QUEUE_CAP: usize = 64;
const READ_CHUNK_FRAMES: i32 = 480;
const READ_TIMEOUT_NS: i64 = 20_000_000;
struct SharedQueue {
frames: Mutex<VecDeque<AudioFrame>>,
}
struct MicSession {
stream_info: StreamInfo,
queue: Arc<SharedQueue>,
stop: Arc<AtomicBool>,
worker: Option<JoinHandle<()>>,
}
pub struct AndroidMicrophoneCapture {
inner: Option<MicSession>,
}
impl AndroidMicrophoneCapture {
pub fn open(config: &AudioCaptureConfig) -> Result<Self, CaptureError> {
if config.select != Select::Default {
return Err(CaptureError::Unsupported);
}
if config.sample_format != SampleFormat::F32 {
return Err(CaptureError::Unsupported);
}
if config.time_base.den == 0 {
return Err(CaptureError::InvalidInput);
}
let queue = Arc::new(SharedQueue {
frames: Mutex::new(VecDeque::new()),
});
let queue_worker = Arc::clone(&queue);
let stop = Arc::new(AtomicBool::new(false));
let stop_worker = Arc::clone(&stop);
let time_base = config.time_base;
let (tx_info, rx_info) = std::sync::mpsc::sync_channel(1);
let worker = thread::Builder::new()
.name("mediaway-aaudio-mic".into())
.spawn(move || {
run_mic_worker(time_base, &queue_worker, &stop_worker, &tx_info);
})
.map_err(|_| CaptureError::Backend)?;
let stream_info = rx_info.recv().map_err(|_| CaptureError::Backend)??;
Ok(Self {
inner: Some(MicSession {
stream_info,
queue,
stop,
worker: Some(worker),
}),
})
}
}
impl AudioCapture for AndroidMicrophoneCapture {
fn stream_info(&self) -> &StreamInfo {
#[allow(
clippy::option_if_let_else,
reason = "map_or_else forces 'static vs 'self lifetime clash"
)]
if let Some(s) = self.inner.as_ref() {
&s.stream_info
} else {
closed_audio_info()
}
}
fn poll_frame(&mut self) -> Result<Option<AudioFrame>, CaptureError> {
let Some(session) = self.inner.as_ref() else {
return Err(CaptureError::Closed);
};
let mut q = session
.queue
.frames
.lock()
.map_err(|_| CaptureError::Backend)?;
Ok(q.pop_front())
}
fn close(&mut self) -> Result<(), CaptureError> {
let Some(mut session) = self.inner.take() else {
return Ok(());
};
session.stop.store(true, Ordering::SeqCst);
if let Some(h) = session.worker.take() {
let _ = h.join();
}
Ok(())
}
}
impl Drop for AndroidMicrophoneCapture {
fn drop(&mut self) {
let _ = self.close();
}
}
fn closed_audio_info() -> &'static StreamInfo {
use std::sync::OnceLock;
static INFO: OnceLock<StreamInfo> = OnceLock::new();
INFO.get_or_init(|| StreamInfo::Audio {
id: 0,
codec: CodecKind::RawAudio,
time_base: Rational::new(1, 48_000),
sample_rate: 0,
channels: 0,
extra_data: Bytes::new(),
})
}
fn run_mic_worker(
time_base: Rational,
queue: &SharedQueue,
stop: &AtomicBool,
tx_info: &SyncSender<Result<StreamInfo, CaptureError>>,
) {
let stream = match open_stream() {
Ok(s) => s,
Err(e) => {
let _ = tx_info.send(Err(e));
return;
}
};
let channels = stream.channel_count();
let sample_rate = stream.sample_rate();
if channels <= 0 || sample_rate <= 0 {
let _ = tx_info.send(Err(CaptureError::Backend));
return;
}
let info = StreamInfo::Audio {
id: 0,
codec: CodecKind::RawAudio,
time_base,
sample_rate: sample_rate.unsigned_abs(),
channels: u16::try_from(channels).unwrap_or(0),
extra_data: Bytes::new(),
};
if stream.request_start().is_err() {
let _ = tx_info.send(Err(CaptureError::Backend));
return;
}
let _ = tx_info.send(Ok(info));
let channels = usize::try_from(channels).unwrap_or(0);
let mut buffer = vec![0f32; READ_CHUNK_FRAMES as usize * channels];
let mut pts: i64 = 0;
while !stop.load(Ordering::Relaxed) {
let read = unsafe {
stream.read(
buffer.as_mut_ptr().cast(),
READ_CHUNK_FRAMES,
READ_TIMEOUT_NS,
)
};
match read {
Ok(0) => {}
Ok(frames) => {
let frames = frames as usize;
let sample_count = frames.saturating_mul(channels);
if let Some(chunk) = buffer.get(..sample_count) {
push_frame(queue, sample_rate.unsigned_abs(), channels, pts, chunk);
pts = pts.saturating_add(i64::try_from(frames).unwrap_or(0));
}
}
Err(_) => break,
}
}
let _ = stream.request_stop();
}
fn open_stream() -> Result<AudioStream, CaptureError> {
AudioStreamBuilder::new()
.map_err(|_| CaptureError::Backend)?
.direction(AudioDirection::Input)
.format(AudioFormat::PCM_Float)
.sharing_mode(AudioSharingMode::Shared)
.performance_mode(AudioPerformanceMode::LowLatency)
.device_id(0)
.open_stream()
.map_err(|_| CaptureError::Backend)
}
fn push_frame(queue: &SharedQueue, sample_rate: u32, channels: usize, pts: i64, samples: &[f32]) {
let frame = AudioFrame {
pts,
duration: u64::try_from(samples.len() / channels.max(1)).unwrap_or(0),
sample_rate,
channels: u16::try_from(channels).unwrap_or(0),
format: SampleFormat::F32,
data: Bytes::copy_from_slice(bytemuck_f32_to_bytes(samples)),
};
if let Ok(mut q) = queue.frames.lock() {
if q.len() >= PCM_QUEUE_CAP {
let _ = q.pop_front();
}
q.push_back(frame);
}
}
const fn bytemuck_f32_to_bytes(samples: &[f32]) -> &[u8] {
unsafe {
std::slice::from_raw_parts(
samples.as_ptr().cast::<u8>(),
std::mem::size_of_val(samples),
)
}
}
#[cfg(test)]
#[path = "mic_tests.rs"]
mod tests;