use portaudio as pa;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
use tokio::sync::mpsc;
use crate::transcription_stats::TranscriptionStats;
use parking_lot::Mutex;
pub struct AudioCapture {
pa_stream: Option<pa::Stream<pa::NonBlocking, pa::Input<f32>>>,
pa: Option<pa::PortAudio>,
input_settings: Option<pa::InputStreamSettings<f32>>,
samples_sent: Arc<AtomicUsize>,
buffer_size: usize,
}
impl AudioCapture {
pub fn new() -> Self {
Self::with_buffer_size(crate::config::AudioProcessorConfig::default().buffer_size)
}
pub fn with_buffer_size(buffer_size: usize) -> Self {
Self {
pa_stream: None,
pa: None,
input_settings: None,
samples_sent: Arc::new(AtomicUsize::new(0)),
buffer_size,
}
}
fn initialize_audio(&mut self) -> Result<(), anyhow::Error> {
if self.pa.is_some() {
return Ok(()); }
let pa = pa::PortAudio::new()
.map_err(|e| anyhow::anyhow!("Failed to initialize PortAudio: {}", e))?;
let input_params = pa
.default_input_stream_params::<f32>(1)
.map_err(|e| anyhow::anyhow!("Failed to get default input stream parameters: {}", e))?;
let frames_per_buffer = u32::try_from(self.buffer_size)
.map_err(|_| anyhow::anyhow!("Audio buffer size too large: {}", self.buffer_size))?;
let input_settings = pa::InputStreamSettings::new(
input_params,
crate::config::SAMPLE_RATE as f64,
frames_per_buffer,
);
self.pa = Some(pa);
self.input_settings = Some(input_settings);
Ok(())
}
pub fn start(
&mut self,
tx: mpsc::Sender<Vec<f32>>,
running: Arc<AtomicBool>,
recording: Arc<AtomicBool>,
transcription_stats: Arc<Mutex<TranscriptionStats>>,
) -> Result<(), anyhow::Error> {
self.initialize_audio()?;
let pa = self
.pa
.as_ref()
.ok_or_else(|| anyhow::anyhow!("PortAudio not initialized"))?;
let input_settings = *self
.input_settings
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Audio input settings not initialized"))?;
let recording_for_callback = recording.clone();
let stats_for_callback = transcription_stats.clone();
let samples_sent_for_callback = self.samples_sent.clone();
let callback = move |pa::InputStreamCallbackArgs { buffer, .. }| {
if recording_for_callback.load(Ordering::Relaxed) {
let samples = buffer.to_vec();
match tx.try_send(samples) {
Ok(_) => {
samples_sent_for_callback.fetch_add(1, Ordering::Release);
}
Err(mpsc::error::TrySendError::Full(_)) => {
if let Some(mut stats) = stats_for_callback.try_lock() {
let total = stats.record_audio_drop(1);
tracing::warn!(
"Audio channel full, dropped samples (total: {})",
total
);
}
}
Err(mpsc::error::TrySendError::Closed(_)) => {
tracing::warn!("Failed to send samples: channel closed");
if let Some(mut stats) = stats_for_callback.try_lock() {
let total = stats.record_audio_drop(1);
tracing::warn!("Audio channel drop recorded (total: {})", total);
}
}
}
}
if running.load(Ordering::Relaxed) {
pa::Continue
} else {
pa::Complete
}
};
let mut stream = pa
.open_non_blocking_stream(input_settings, callback)
.map_err(|e| anyhow::anyhow!("Failed to open stream: {}", e))?;
if recording.load(Ordering::Relaxed) {
stream
.start()
.map_err(|e| anyhow::anyhow!("Failed to start stream: {}", e))?;
}
self.pa_stream = Some(stream);
Ok(())
}
pub fn start_recording(&mut self) -> Result<(), anyhow::Error> {
if let Some(stream) = &mut self.pa_stream {
if !stream.is_active().unwrap_or(false) {
stream
.start()
.map_err(|e| anyhow::anyhow!("Failed to start recording: {}", e))?;
}
}
Ok(())
}
pub fn stop_recording(&mut self) -> Result<(), anyhow::Error> {
if let Some(stream) = &mut self.pa_stream {
if stream.is_active().unwrap_or(false) {
stream
.stop()
.map_err(|e| anyhow::anyhow!("Failed to stop recording: {}", e))?;
}
}
Ok(())
}
pub fn get_samples_sent_count(&self) -> Arc<AtomicUsize> {
self.samples_sent.clone()
}
pub fn pause(&mut self) -> Result<(), anyhow::Error> {
self.stop_recording()
}
pub fn resume(&mut self) -> Result<(), anyhow::Error> {
self.start_recording()
}
pub fn stop(&mut self) {
if let Some(stream) = &mut self.pa_stream {
if let Err(e) = stream.stop() {
tracing::warn!("Failed to stop stream: {}", e);
}
if let Err(e) = stream.close() {
tracing::warn!("Failed to close stream: {}", e);
}
}
self.pa_stream = None;
self.pa = None;
self.input_settings = None;
}
}
impl Default for AudioCapture {
fn default() -> Self {
Self::new()
}
}
impl Drop for AudioCapture {
fn drop(&mut self) {
self.stop();
}
}