Initialize project and implement decentralized voice chat client (PipeWire, Opus, Iroh, Iced)

This commit is contained in:
2026-05-27 05:18:56 -04:00
commit 1220d94e91
15 changed files with 9177 additions and 0 deletions
+31
View File
@@ -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;
+328
View File
@@ -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(())
}