use super::AudioConfig;
use anyhow::{anyhow, Result};
use libpulse_binding as pulse;
use libpulse_simple_binding::Simple;
use ringbuf::traits::Producer;
use ringbuf::HeapProd;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread;
pub struct AudioTap {
running: Arc<AtomicBool>,
capture_thread: Option<thread::JoinHandle<()>>,
}
unsafe impl Send for AudioTap {}
impl AudioTap {
pub fn new(producer: HeapProd<f32>, config: AudioConfig) -> Result<Self> {
let running = Arc::new(AtomicBool::new(true));
let running_clone = running.clone();
let channels = config.channels;
let sample_rate = config.sample_rate as u32;
let spec = pulse::sample::Spec {
format: pulse::sample::Format::F32le,
channels: channels as u8,
rate: sample_rate,
};
if !spec.is_valid() {
return Err(anyhow!(
"Invalid PulseAudio sample spec: {}Hz, {} channels",
sample_rate,
channels
));
}
let simple = Simple::new(
None, "terminal-vibes", pulse::stream::Direction::Record, Some("@DEFAULT_SINK@.monitor"), "audio-capture", &spec,
None, None, )
.map_err(|e| {
anyhow!(
"Failed to connect to PulseAudio: {}. \
Is PulseAudio or PipeWire running?",
e
)
})?;
log::info!(
"PulseAudio monitor capture connected (sample_rate={}, channels={})",
sample_rate,
channels
);
let capture_thread = thread::spawn(move || {
Self::capture_loop(simple, producer, channels, &running_clone);
});
Ok(Self {
running,
capture_thread: Some(capture_thread),
})
}
fn capture_loop(
simple: Simple,
mut producer: HeapProd<f32>,
channels: u32,
running: &AtomicBool,
) {
let buf_frames = 1764;
let buf_size = buf_frames * channels as usize;
let mut buf = vec![0.0f32; buf_size];
let byte_len = buf_size * std::mem::size_of::<f32>();
while running.load(Ordering::Relaxed) {
let byte_slice =
unsafe { std::slice::from_raw_parts_mut(buf.as_mut_ptr() as *mut u8, byte_len) };
match simple.read(byte_slice) {
Ok(()) => {}
Err(e) => {
log::warn!("PulseAudio read error: {}", e);
break;
}
}
if channels >= 2 {
for chunk in buf.chunks(channels as usize) {
let mono = chunk.iter().sum::<f32>() / channels as f32;
let _ = producer.try_push(mono);
}
} else {
for &sample in &buf {
let _ = producer.try_push(sample);
}
}
}
log::info!("PulseAudio capture loop ended");
}
}
impl Drop for AudioTap {
fn drop(&mut self) {
self.running.store(false, Ordering::Relaxed);
if let Some(handle) = self.capture_thread.take() {
let _ = handle.join();
}
log::info!("PulseAudio monitor capture stopped");
}
}