#[cfg(feature = "capture")]
use std::sync::atomic::{AtomicBool, Ordering};
#[cfg(feature = "capture")]
use std::sync::Arc;
#[cfg(feature = "capture")]
use std::time::{Duration, Instant};
#[cfg(feature = "capture")]
use cpal::traits::{DeviceTrait, StreamTrait};
#[cfg(feature = "capture")]
use crossbeam_channel::{Receiver, RecvTimeoutError, Sender, TryRecvError};
use crate::device::DeviceSelector;
use crate::error::DecibriError;
#[derive(Debug, Clone)]
pub struct MicrophoneConfig {
pub sample_rate: u32,
pub channels: u16,
pub frames_per_buffer: u32,
pub device: DeviceSelector,
}
impl Default for MicrophoneConfig {
fn default() -> Self {
Self {
sample_rate: 16000,
channels: 1,
frames_per_buffer: 1600,
device: DeviceSelector::Default,
}
}
}
impl MicrophoneConfig {
pub fn validate(&self) -> Result<(), DecibriError> {
if !(1000..=384000).contains(&self.sample_rate) {
return Err(DecibriError::SampleRateOutOfRange);
}
if !(1..=32).contains(&self.channels) {
return Err(DecibriError::ChannelsOutOfRange);
}
if !(64..=65536).contains(&self.frames_per_buffer) {
return Err(DecibriError::FramesPerBufferOutOfRange);
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct AudioChunk {
pub data: Vec<f32>,
pub sample_rate: u32,
pub channels: u16,
}
#[cfg(feature = "capture")]
pub struct MicrophoneStream {
_stream: Option<cpal::Stream>,
receiver: Receiver<AudioChunk>,
running: Arc<AtomicBool>,
#[allow(dead_code)]
sample_rate: u32,
#[allow(dead_code)]
channels: u16,
}
#[cfg(feature = "capture")]
impl MicrophoneStream {
pub fn receiver(&self) -> &Receiver<AudioChunk> {
&self.receiver
}
pub fn try_next_chunk(&self) -> Result<Option<AudioChunk>, DecibriError> {
match self.receiver.try_recv() {
Ok(chunk) => Ok(Some(chunk)),
Err(TryRecvError::Empty) => {
if self.is_open() {
Ok(None)
} else {
Err(DecibriError::MicrophoneStreamClosed)
}
}
Err(TryRecvError::Disconnected) => Err(DecibriError::MicrophoneStreamClosed),
}
}
pub fn next_chunk(
&self,
timeout: Option<Duration>,
) -> Result<Option<AudioChunk>, DecibriError> {
const POLL_INTERVAL: Duration = Duration::from_millis(20);
let deadline = timeout.map(|t| Instant::now() + t);
loop {
let wait = match deadline {
Some(dl) => {
let now = Instant::now();
if now >= dl {
return Ok(self.receiver.try_recv().ok());
}
std::cmp::min(dl - now, POLL_INTERVAL)
}
None => POLL_INTERVAL,
};
match self.receiver.recv_timeout(wait) {
Ok(chunk) => return Ok(Some(chunk)),
Err(RecvTimeoutError::Timeout) => {
if !self.is_open() {
return match self.receiver.try_recv() {
Ok(chunk) => Ok(Some(chunk)),
Err(_) => Err(DecibriError::MicrophoneStreamClosed),
};
}
}
Err(RecvTimeoutError::Disconnected) => {
return Err(DecibriError::MicrophoneStreamClosed);
}
}
}
}
pub fn is_open(&self) -> bool {
self.running.load(Ordering::Relaxed)
}
pub fn stop(&self) {
self.running.store(false, Ordering::Relaxed);
}
}
#[cfg(feature = "capture")]
pub struct Microphone {
config: MicrophoneConfig,
device: cpal::Device,
}
#[cfg(feature = "capture")]
impl Microphone {
pub fn new(config: MicrophoneConfig) -> Result<Self, DecibriError> {
config.validate()?;
let device = crate::device::resolve_device(&config.device)?;
Ok(Self { config, device })
}
pub fn devices() -> Result<Vec<crate::device::MicrophoneInfo>, DecibriError> {
crate::device::input_devices()
}
pub fn start(&self) -> Result<MicrophoneStream, DecibriError> {
let (sender, receiver): (Sender<AudioChunk>, Receiver<AudioChunk>) =
crossbeam_channel::unbounded();
let running = Arc::new(AtomicBool::new(true));
let running_clone = running.clone();
let sample_rate = self.config.sample_rate;
let channels = self.config.channels;
let frames_per_buffer = self.config.frames_per_buffer;
let stream_config = cpal::StreamConfig {
channels,
sample_rate,
buffer_size: cpal::BufferSize::Fixed(frames_per_buffer),
};
let err_running = running.clone();
let stream = self
.device
.build_input_stream(
&stream_config,
move |data: &[f32], _: &cpal::InputCallbackInfo| {
if !running_clone.load(Ordering::Relaxed) {
return;
}
let chunk = AudioChunk {
data: data.to_vec(),
sample_rate,
channels,
};
let _ = sender.send(chunk);
},
move |err| {
eprintln!("decibri: audio stream error: {err}");
err_running.store(false, Ordering::Relaxed);
},
None, )
.map_err(|e| DecibriError::StreamOpenFailed(e.to_string()))?;
stream
.play()
.map_err(|e| DecibriError::StreamStartFailed(e.to_string()))?;
Ok(MicrophoneStream {
_stream: Some(stream),
receiver,
running,
sample_rate,
channels,
})
}
}
#[cfg(all(test, feature = "capture"))]
mod tests {
use super::*;
use std::thread;
fn test_stream() -> (MicrophoneStream, Sender<AudioChunk>, Arc<AtomicBool>) {
let (sender, receiver) = crossbeam_channel::unbounded::<AudioChunk>();
let running = Arc::new(AtomicBool::new(true));
let stream = MicrophoneStream {
_stream: None,
receiver,
running: running.clone(),
sample_rate: 16000,
channels: 1,
};
(stream, sender, running)
}
fn make_chunk(first_sample: f32) -> AudioChunk {
AudioChunk {
data: vec![first_sample],
sample_rate: 16000,
channels: 1,
}
}
#[test]
fn test_try_next_chunk_returns_none_when_empty() {
let (stream, _sender, _running) = test_stream();
let result = stream.try_next_chunk().unwrap();
assert!(
result.is_none(),
"try_next_chunk on empty open stream should return Ok(None)"
);
}
#[test]
fn test_try_next_chunk_returns_chunk_when_available() {
let (stream, sender, _running) = test_stream();
sender.send(make_chunk(0.42)).unwrap();
let result = stream.try_next_chunk().unwrap();
let chunk = result.expect("should have received the injected chunk");
assert_eq!(chunk.data, vec![0.42]);
}
#[test]
fn test_try_next_chunk_returns_err_when_closed() {
let (stream, sender, running) = test_stream();
drop(sender); running.store(false, Ordering::Relaxed);
let err = stream.try_next_chunk().unwrap_err();
assert!(matches!(err, DecibriError::MicrophoneStreamClosed));
}
#[test]
fn test_next_chunk_blocks_until_chunk_arrives() {
let (stream, sender, _running) = test_stream();
let producer = thread::spawn(move || {
thread::sleep(Duration::from_millis(50));
sender.send(make_chunk(0.77)).unwrap();
});
let result = stream.next_chunk(None).unwrap();
let chunk = result.expect("should have received the eventually-pushed chunk");
assert_eq!(chunk.data, vec![0.77]);
producer.join().unwrap();
}
#[test]
fn test_next_chunk_timeout_returns_none() {
let (stream, _sender, _running) = test_stream();
let start = Instant::now();
let result = stream.next_chunk(Some(Duration::from_millis(50))).unwrap();
let elapsed = start.elapsed();
assert!(
result.is_none(),
"next_chunk with timeout and no arrivals should return Ok(None)"
);
assert!(
elapsed >= Duration::from_millis(40),
"next_chunk returned too early: {elapsed:?}"
);
}
#[test]
fn test_next_chunk_flushes_buffered_before_closed_err() {
let (stream, sender, running) = test_stream();
sender.send(make_chunk(1.0)).unwrap();
sender.send(make_chunk(2.0)).unwrap();
drop(sender); running.store(false, Ordering::Relaxed);
let c1 = stream.next_chunk(Some(Duration::from_millis(100))).unwrap();
assert_eq!(c1.unwrap().data, vec![1.0]);
let c2 = stream.next_chunk(Some(Duration::from_millis(100))).unwrap();
assert_eq!(c2.unwrap().data, vec![2.0]);
let err = stream
.next_chunk(Some(Duration::from_millis(100)))
.unwrap_err();
assert!(matches!(err, DecibriError::MicrophoneStreamClosed));
}
#[test]
fn test_next_chunk_returns_closed_within_polling_interval_after_stop() {
let (stream, _sender, running) = test_stream();
let r = running.clone();
let stopper = thread::spawn(move || {
thread::sleep(Duration::from_millis(30));
r.store(false, Ordering::Relaxed);
});
let start = Instant::now();
let err = stream.next_chunk(None).unwrap_err();
let elapsed = start.elapsed();
assert!(matches!(err, DecibriError::MicrophoneStreamClosed));
assert!(
elapsed < Duration::from_millis(250),
"next_chunk took too long to detect stop(): {elapsed:?}"
);
stopper.join().unwrap();
}
#[test]
fn test_arc_mutex_microphone_stream_is_send_and_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Arc<std::sync::Mutex<MicrophoneStream>>>();
}
#[test]
fn test_arc_mutex_microphone_stream_serializes_two_threads() {
use std::sync::{Barrier, Mutex};
let (stream, sender, _running) = test_stream();
sender.send(make_chunk(1.0)).unwrap();
sender.send(make_chunk(2.0)).unwrap();
let shared = Arc::new(Mutex::new(stream));
let barrier = Arc::new(Barrier::new(2));
let s1 = shared.clone();
let b1 = barrier.clone();
let t1 = thread::spawn(move || {
b1.wait();
let guard = s1.lock().unwrap();
guard.try_next_chunk().unwrap()
});
let s2 = shared.clone();
let b2 = barrier.clone();
let t2 = thread::spawn(move || {
b2.wait();
let guard = s2.lock().unwrap();
guard.try_next_chunk().unwrap()
});
let c1 = t1.join().unwrap().expect("thread 1 must receive a chunk");
let c2 = t2.join().unwrap().expect("thread 2 must receive a chunk");
let mut vals = [c1.data[0], c2.data[0]];
vals.sort_by(|a, b| a.partial_cmp(b).unwrap());
assert_eq!(
vals,
[1.0, 2.0],
"both chunks must be consumed exactly once with no duplicates or losses"
);
}
}