use super::{AudioCapture, CHANNEL_CAPACITY, CHUNK_DURATION_MS};
use crate::config::AudioConfig;
use crate::error::TalkError;
use tokio::sync::mpsc;
use pipewire as pw;
use pw::spa;
use spa::param::audio::{AudioFormat, AudioInfoRaw};
use spa::pod::{self, Pod, Value};
use spa::utils::Direction;
struct Terminate;
pub struct PipeWireCapture {
config: AudioConfig,
thread_handle: Option<std::thread::JoinHandle<()>>,
quit_sender: Option<pw::channel::Sender<Terminate>>,
}
impl PipeWireCapture {
pub fn new(config: AudioConfig) -> Self {
Self {
config,
thread_handle: None,
quit_sender: None,
}
}
}
impl AudioCapture for PipeWireCapture {
fn start(&mut self) -> Result<mpsc::Receiver<Vec<i16>>, TalkError> {
if self.thread_handle.is_some() {
return Err(TalkError::Audio("PipeWire capture already running".into()));
}
let (audio_tx, audio_rx) = mpsc::channel(CHANNEL_CAPACITY);
let (quit_tx, quit_rx) = pw::channel::channel::<Terminate>();
let rate = self.config.sample_rate;
let channels = self.config.channels;
let frames_per_chunk = (rate as usize * CHUNK_DURATION_MS as usize) / 1000;
let samples_per_chunk = frames_per_chunk * channels as usize;
let handle = std::thread::Builder::new()
.name("pipewire-capture".into())
.spawn(move || {
if let Err(e) =
run_capture_loop(quit_rx, audio_tx, rate, channels, samples_per_chunk)
{
log::error!("PipeWire capture error: {}", e);
}
})
.map_err(|e| TalkError::Audio(format!("failed to spawn PipeWire thread: {}", e)))?;
self.thread_handle = Some(handle);
self.quit_sender = Some(quit_tx);
Ok(audio_rx)
}
fn stop(&mut self) -> Result<(), TalkError> {
if let Some(tx) = self.quit_sender.take() {
let _ = tx.send(Terminate);
}
if let Some(handle) = self.thread_handle.take() {
if handle.join().is_err() {
log::warn!("PipeWire capture thread panicked");
}
}
Ok(())
}
}
unsafe impl Send for PipeWireCapture {}
fn run_capture_loop(
quit_rx: pw::channel::Receiver<Terminate>,
audio_tx: mpsc::Sender<Vec<i16>>,
rate: u32,
channels: u8,
samples_per_chunk: usize,
) -> Result<(), TalkError> {
let mainloop = pw::main_loop::MainLoopRc::new(None)
.map_err(|e| TalkError::Audio(format!("PipeWire MainLoop: {}", e)))?;
let _quit = quit_rx.attach(mainloop.loop_(), {
let ml = mainloop.clone();
move |_| ml.quit()
});
let context = pw::context::ContextRc::new(&mainloop, None)
.map_err(|e| TalkError::Audio(format!("PipeWire Context: {}", e)))?;
let core = context
.connect_rc(None)
.map_err(|e| TalkError::Audio(format!("PipeWire connect: {}", e)))?;
let props = pw::properties::properties! {
*pw::keys::MEDIA_TYPE => "Audio",
*pw::keys::MEDIA_CATEGORY => "Capture",
*pw::keys::MEDIA_ROLE => "Speech",
};
let stream = pw::stream::StreamRc::new(core.clone(), "talk-rs", props)
.map_err(|e| TalkError::Audio(format!("PipeWire Stream: {}", e)))?;
use std::cell::RefCell;
use std::rc::Rc;
let buffer: Rc<RefCell<Vec<i16>>> =
Rc::new(RefCell::new(Vec::with_capacity(samples_per_chunk * 2)));
let _listener = stream
.add_local_listener()
.process({
let buf = Rc::clone(&buffer);
let audio_tx = audio_tx.clone();
move |stream_ref, _: &mut ()| {
let Some(mut pw_buf) = stream_ref.dequeue_buffer() else {
return;
};
let datas = pw_buf.datas_mut();
if datas.is_empty() {
return;
}
let size = datas[0].chunk().size() as usize;
let Some(raw) = datas[0].data() else {
return;
};
let pcm = &raw[..size];
let mut buffer = buf.borrow_mut();
buffer.extend(
pcm.chunks_exact(2)
.map(|pair| i16::from_le_bytes([pair[0], pair[1]])),
);
while buffer.len() >= samples_per_chunk {
let chunk: Vec<i16> = buffer.drain(..samples_per_chunk).collect();
if audio_tx.try_send(chunk).is_err() {
log::warn!("audio channel full, dropped {} samples", samples_per_chunk);
}
}
}
})
.register()
.map_err(|e| TalkError::Audio(format!("PipeWire listener: {}", e)))?;
let mut audio_info = AudioInfoRaw::new();
audio_info.set_format(AudioFormat::S16LE);
audio_info.set_rate(rate);
audio_info.set_channels(channels as u32);
let format_bytes = serialize_audio_info(audio_info)?;
let format_pod = Pod::from_bytes(&format_bytes)
.ok_or_else(|| TalkError::Audio("invalid audio format pod".into()))?;
stream
.connect(
Direction::Input,
None, pw::stream::StreamFlags::AUTOCONNECT | pw::stream::StreamFlags::MAP_BUFFERS,
&mut [format_pod],
)
.map_err(|e| TalkError::Audio(format!("PipeWire stream connect: {}", e)))?;
log::info!(
"PipeWire capture started: {}Hz, {} ch, s16le",
rate,
channels
);
mainloop.run();
let remaining = buffer.borrow_mut().split_off(0);
if !remaining.is_empty() {
log::debug!(
"flushing {} residual samples ({} full chunks + {} trailing)",
remaining.len(),
remaining.len() / samples_per_chunk,
remaining.len() % samples_per_chunk,
);
for chunk in remaining.chunks(samples_per_chunk) {
if audio_tx.try_send(chunk.to_vec()).is_err() {
log::warn!(
"audio channel full while flushing, dropped {} samples",
chunk.len()
);
}
}
}
log::debug!("PipeWire capture mainloop exited");
Ok(())
}
pub(crate) fn serialize_audio_info(info: AudioInfoRaw) -> Result<Vec<u8>, TalkError> {
use spa::pod::serialize::PodSerializer;
use std::io;
PodSerializer::serialize(
io::Cursor::new(Vec::new()),
&Value::Object(pod::Object {
type_: spa::utils::SpaTypes::ObjectParamFormat.as_raw(),
id: spa::param::ParamType::EnumFormat.as_raw(),
properties: info.into(),
}),
)
.map(|(cursor, _)| cursor.into_inner())
.map_err(|e| TalkError::Audio(format!("audio format serialization: {:?}", e)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_serialize_audio_info() {
let mut info = AudioInfoRaw::new();
info.set_format(AudioFormat::S16LE);
info.set_rate(48_000);
info.set_channels(1);
let bytes = serialize_audio_info(info);
assert!(bytes.is_ok());
let bytes = bytes.unwrap();
assert!(!bytes.is_empty());
let pod = Pod::from_bytes(&bytes);
assert!(pod.is_some());
}
}