Initialize project and implement decentralized voice chat client (PipeWire, Opus, Iroh, Iced)
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
use std::sync::mpsc::{Sender, Receiver};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AudioError {
|
||||
#[error("Failed to initialize audio backend: {0}")]
|
||||
Init(String),
|
||||
#[error("Audio device error: {0}")]
|
||||
Device(String),
|
||||
#[error("Stream error: {0}")]
|
||||
Stream(String),
|
||||
#[error("Audio buffer overflow/underflow")]
|
||||
BufferError,
|
||||
#[error("Other audio error: {0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
pub trait AudioBackend: Send + Sync {
|
||||
/// Starts capturing raw PCM audio from the input device (microphone),
|
||||
/// sending chunks of samples (e.g. `Vec<i16>`) to the provided Sender.
|
||||
fn start_capture(&self, tx: Sender<Vec<i16>>) -> Result<(), AudioError>;
|
||||
|
||||
/// Starts playing back raw PCM audio to the output device (speaker),
|
||||
/// reading mixed/incoming chunks of samples from the provided Receiver.
|
||||
fn start_playback(&self, rx: Receiver<Vec<i16>>) -> Result<(), AudioError>;
|
||||
|
||||
/// Stops both capture and playback streams.
|
||||
fn stop(&self) -> Result<(), AudioError>;
|
||||
}
|
||||
|
||||
pub mod pipewire_impl;
|
||||
@@ -0,0 +1,328 @@
|
||||
use crate::audio::{AudioBackend, AudioError};
|
||||
use std::sync::mpsc::{Sender, Receiver};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::thread::{self, JoinHandle};
|
||||
use std::time::Duration;
|
||||
use pipewire as pw;
|
||||
use pw::{properties::properties, spa};
|
||||
use spa::pod::Pod;
|
||||
use ringbuf::{HeapRb, traits::{Consumer, Producer, Split}};
|
||||
|
||||
pub struct PipeWireBackend {
|
||||
capture_state: Mutex<Option<CaptureState>>,
|
||||
playback_state: Mutex<Option<PlaybackState>>,
|
||||
}
|
||||
|
||||
struct CaptureState {
|
||||
cmd_tx: pw::channel::Sender<()>,
|
||||
thread: JoinHandle<()>,
|
||||
}
|
||||
|
||||
struct PlaybackState {
|
||||
cmd_tx: pw::channel::Sender<()>,
|
||||
thread: JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl PipeWireBackend {
|
||||
pub fn new() -> Self {
|
||||
pw::init();
|
||||
Self {
|
||||
capture_state: Mutex::new(None),
|
||||
playback_state: Mutex::new(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioBackend for PipeWireBackend {
|
||||
fn start_capture(&self, tx: Sender<Vec<i16>>) -> Result<(), AudioError> {
|
||||
let mut capture_guard = self.capture_state.lock().unwrap();
|
||||
if capture_guard.is_some() {
|
||||
return Err(AudioError::Stream("Capture already started".to_string()));
|
||||
}
|
||||
|
||||
let (cmd_tx, cmd_rx) = pw::channel::channel::<()>();
|
||||
let tx_clone = tx.clone();
|
||||
|
||||
let thread = thread::Builder::new()
|
||||
.name("peerspeak-capture".to_string())
|
||||
.spawn(move || {
|
||||
if let Err(e) = run_capture(cmd_rx, tx_clone) {
|
||||
eprintln!("Capture thread error: {:?}", e);
|
||||
}
|
||||
})
|
||||
.map_err(|e| AudioError::Init(e.to_string()))?;
|
||||
|
||||
*capture_guard = Some(CaptureState { cmd_tx, thread });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn start_playback(&self, rx: Receiver<Vec<i16>>) -> Result<(), AudioError> {
|
||||
let mut playback_guard = self.playback_state.lock().unwrap();
|
||||
if playback_guard.is_some() {
|
||||
return Err(AudioError::Stream("Playback already started".to_string()));
|
||||
}
|
||||
|
||||
let (cmd_tx, cmd_rx) = pw::channel::channel::<()>();
|
||||
|
||||
let thread = thread::Builder::new()
|
||||
.name("peerspeak-playback".to_string())
|
||||
.spawn(move || {
|
||||
if let Err(e) = run_playback(cmd_rx, rx) {
|
||||
eprintln!("Playback thread error: {:?}", e);
|
||||
}
|
||||
})
|
||||
.map_err(|e| AudioError::Init(e.to_string()))?;
|
||||
|
||||
*playback_guard = Some(PlaybackState { cmd_tx, thread });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stop(&self) -> Result<(), AudioError> {
|
||||
// Stop capture
|
||||
let mut capture_guard = self.capture_state.lock().unwrap();
|
||||
if let Some(state) = capture_guard.take() {
|
||||
let _ = state.cmd_tx.send(());
|
||||
let _ = state.thread.join();
|
||||
}
|
||||
|
||||
// Stop playback
|
||||
let mut playback_guard = self.playback_state.lock().unwrap();
|
||||
if let Some(state) = playback_guard.take() {
|
||||
let _ = state.cmd_tx.send(());
|
||||
let _ = state.thread.join();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn run_capture(cmd_rx: pw::channel::Receiver<()>, tx: Sender<Vec<i16>>) -> Result<(), AudioError> {
|
||||
let mainloop = pw::main_loop::MainLoopRc::new(None)
|
||||
.map_err(|e| AudioError::Init(e.to_string()))?;
|
||||
let context = pw::context::ContextRc::new(&mainloop, None)
|
||||
.map_err(|e| AudioError::Init(e.to_string()))?;
|
||||
let core = context.connect_rc(None)
|
||||
.map_err(|e| AudioError::Init(e.to_string()))?;
|
||||
|
||||
// Ring buffer setup: 9600 samples (200ms capacity for mono 48kHz)
|
||||
let rb = HeapRb::<i16>::new(9600);
|
||||
let (producer, mut consumer) = rb.split();
|
||||
|
||||
// Command receiver to quit main loop
|
||||
let mainloop_clone = mainloop.clone();
|
||||
let _cmd_recv = cmd_rx.attach(mainloop.loop_(), move |_| {
|
||||
mainloop_clone.quit();
|
||||
});
|
||||
|
||||
let props = properties! {
|
||||
*pw::keys::MEDIA_TYPE => "Audio",
|
||||
*pw::keys::MEDIA_CATEGORY => "Capture",
|
||||
*pw::keys::MEDIA_ROLE => "Communication",
|
||||
};
|
||||
|
||||
let stream = pw::stream::StreamBox::new(&core, "peerspeak-capture-stream", props)
|
||||
.map_err(|e| AudioError::Init(e.to_string()))?;
|
||||
|
||||
struct CaptureUserData<P: ringbuf::traits::Producer<Item = i16>> {
|
||||
producer: P,
|
||||
}
|
||||
|
||||
let _listener = stream
|
||||
.add_local_listener_with_user_data(CaptureUserData { producer })
|
||||
.process(|stream, user_data| {
|
||||
if let Some(mut buffer) = stream.dequeue_buffer() {
|
||||
let datas = buffer.datas_mut();
|
||||
if !datas.is_empty() {
|
||||
let data = &mut datas[0];
|
||||
let size = data.chunk().size() as usize;
|
||||
if let Some(slice) = data.data() {
|
||||
// Each sample is 2 bytes (S16LE)
|
||||
for chunk in slice[..size].chunks_exact(2) {
|
||||
let sample = i16::from_le_bytes([chunk[0], chunk[1]]);
|
||||
let _ = user_data.producer.try_push(sample);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.register()
|
||||
.map_err(|e| AudioError::Init(e.to_string()))?;
|
||||
|
||||
let mut audio_info = spa::param::audio::AudioInfoRaw::new();
|
||||
audio_info.set_format(spa::param::audio::AudioFormat::S16LE);
|
||||
audio_info.set_rate(48000);
|
||||
audio_info.set_channels(1); // Mono
|
||||
|
||||
let obj = pw::spa::pod::Object {
|
||||
type_: pw::spa::utils::SpaTypes::ObjectParamFormat.as_raw(),
|
||||
id: pw::spa::param::ParamType::EnumFormat.as_raw(),
|
||||
properties: audio_info.into(),
|
||||
};
|
||||
let values: Vec<u8> = pw::spa::pod::serialize::PodSerializer::serialize(
|
||||
std::io::Cursor::new(Vec::new()),
|
||||
&pw::spa::pod::Value::Object(obj),
|
||||
)
|
||||
.unwrap()
|
||||
.0
|
||||
.into_inner();
|
||||
|
||||
let mut params = [Pod::from_bytes(&values).unwrap()];
|
||||
|
||||
stream.connect(
|
||||
spa::utils::Direction::Input,
|
||||
None,
|
||||
pw::stream::StreamFlags::AUTOCONNECT
|
||||
| pw::stream::StreamFlags::MAP_BUFFERS
|
||||
| pw::stream::StreamFlags::RT_PROCESS,
|
||||
&mut params,
|
||||
)
|
||||
.map_err(|e| AudioError::Stream(e.to_string()))?;
|
||||
|
||||
// Spawn the worker thread to pop from consumer and send Vec<i16> frames
|
||||
let running = Arc::new(AtomicBool::new(true));
|
||||
let running_clone = running.clone();
|
||||
let worker_handle = thread::spawn(move || {
|
||||
let mut frame = Vec::with_capacity(960);
|
||||
while running_clone.load(Ordering::Relaxed) {
|
||||
let mut popped = false;
|
||||
while let Some(sample) = consumer.try_pop() {
|
||||
popped = true;
|
||||
frame.push(sample);
|
||||
if frame.len() == 960 {
|
||||
if tx.send(frame).is_err() {
|
||||
return;
|
||||
}
|
||||
frame = Vec::with_capacity(960);
|
||||
}
|
||||
}
|
||||
if !popped {
|
||||
thread::sleep(Duration::from_millis(5));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
mainloop.run();
|
||||
|
||||
running.store(false, Ordering::Relaxed);
|
||||
let _ = worker_handle.join();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_playback(cmd_rx: pw::channel::Receiver<()>, rx: Receiver<Vec<i16>>) -> Result<(), AudioError> {
|
||||
let mainloop = pw::main_loop::MainLoopRc::new(None)
|
||||
.map_err(|e| AudioError::Init(e.to_string()))?;
|
||||
let context = pw::context::ContextRc::new(&mainloop, None)
|
||||
.map_err(|e| AudioError::Init(e.to_string()))?;
|
||||
let core = context.connect_rc(None)
|
||||
.map_err(|e| AudioError::Init(e.to_string()))?;
|
||||
|
||||
// Ring buffer setup: 9600 samples (200ms capacity for mono 48kHz)
|
||||
let rb = HeapRb::<i16>::new(9600);
|
||||
let (mut producer, consumer) = rb.split();
|
||||
|
||||
// Command receiver to quit main loop
|
||||
let mainloop_clone = mainloop.clone();
|
||||
let _cmd_recv = cmd_rx.attach(mainloop.loop_(), move |_| {
|
||||
mainloop_clone.quit();
|
||||
});
|
||||
|
||||
let props = properties! {
|
||||
*pw::keys::MEDIA_TYPE => "Audio",
|
||||
*pw::keys::MEDIA_CATEGORY => "Playback",
|
||||
*pw::keys::MEDIA_ROLE => "Communication",
|
||||
};
|
||||
|
||||
let stream = pw::stream::StreamBox::new(&core, "peerspeak-playback-stream", props)
|
||||
.map_err(|e| AudioError::Init(e.to_string()))?;
|
||||
|
||||
struct PlaybackUserData<C: ringbuf::traits::Consumer<Item = i16>> {
|
||||
consumer: C,
|
||||
}
|
||||
|
||||
let _listener = stream
|
||||
.add_local_listener_with_user_data(PlaybackUserData { consumer })
|
||||
.process(|stream, user_data| {
|
||||
if let Some(mut buffer) = stream.dequeue_buffer() {
|
||||
let datas = buffer.datas_mut();
|
||||
if !datas.is_empty() {
|
||||
let data = &mut datas[0];
|
||||
let mut total_size = 0;
|
||||
if let Some(slice) = data.data() {
|
||||
let stride = 2; // S16LE Mono = 2 bytes per frame
|
||||
let n_frames = slice.len() / stride;
|
||||
for i in 0..n_frames {
|
||||
let val = user_data.consumer.try_pop().unwrap_or(0);
|
||||
let bytes = val.to_le_bytes();
|
||||
let start = i * stride;
|
||||
slice[start] = bytes[0];
|
||||
slice[start + 1] = bytes[1];
|
||||
}
|
||||
total_size = n_frames * stride;
|
||||
}
|
||||
let chunk = data.chunk_mut();
|
||||
*chunk.offset_mut() = 0;
|
||||
*chunk.stride_mut() = 2;
|
||||
*chunk.size_mut() = total_size as _;
|
||||
}
|
||||
}
|
||||
})
|
||||
.register()
|
||||
.map_err(|e| AudioError::Init(e.to_string()))?;
|
||||
|
||||
let mut audio_info = spa::param::audio::AudioInfoRaw::new();
|
||||
audio_info.set_format(spa::param::audio::AudioFormat::S16LE);
|
||||
audio_info.set_rate(48000);
|
||||
audio_info.set_channels(1); // Mono
|
||||
|
||||
let obj = pw::spa::pod::Object {
|
||||
type_: pw::spa::utils::SpaTypes::ObjectParamFormat.as_raw(),
|
||||
id: pw::spa::param::ParamType::EnumFormat.as_raw(),
|
||||
properties: audio_info.into(),
|
||||
};
|
||||
let values: Vec<u8> = pw::spa::pod::serialize::PodSerializer::serialize(
|
||||
std::io::Cursor::new(Vec::new()),
|
||||
&pw::spa::pod::Value::Object(obj),
|
||||
)
|
||||
.unwrap()
|
||||
.0
|
||||
.into_inner();
|
||||
|
||||
let mut params = [Pod::from_bytes(&values).unwrap()];
|
||||
|
||||
stream.connect(
|
||||
spa::utils::Direction::Output,
|
||||
None,
|
||||
pw::stream::StreamFlags::AUTOCONNECT
|
||||
| pw::stream::StreamFlags::MAP_BUFFERS
|
||||
| pw::stream::StreamFlags::RT_PROCESS,
|
||||
&mut params,
|
||||
)
|
||||
.map_err(|e| AudioError::Stream(e.to_string()))?;
|
||||
|
||||
// Spawn a worker thread to read from rx and push to producer
|
||||
let running = Arc::new(AtomicBool::new(true));
|
||||
let running_clone = running.clone();
|
||||
let worker_handle = thread::spawn(move || {
|
||||
while running_clone.load(Ordering::Relaxed) {
|
||||
if let Ok(frame) = rx.recv() {
|
||||
for &sample in &frame {
|
||||
// Try to push. If buffer is full, drop to avoid growing latency.
|
||||
if producer.try_push(sample).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
mainloop.run();
|
||||
|
||||
running.store(false, Ordering::Relaxed);
|
||||
let _ = worker_handle.join();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user