Files
peerspeak/src/audio/mod.rs
T
2026-06-21 01:05:24 -04:00

117 lines
4.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use std::sync::mpsc::{Receiver, Sender};
use std::sync::Arc;
use std::sync::atomic::AtomicUsize;
use thiserror::Error;
/// Playback output channel count. Capture/encode/network remain mono; only the
/// listener-side playout bus is stereo.
pub const PLAYBACK_CHANNELS: usize = 2;
/// Target depth of the playback ring buffer, in interleaved samples (48kHz
/// stereo).
///
/// The playout chain is paced to keep the ring near this level: production is
/// driven by how fast PipeWire actually drains the ring (the hardware clock),
/// not by a fixed software timer — which is what eliminates the producer/
/// consumer beat that otherwise churns ~20% of audio into drops + silence.
/// 5760 = 60ms = 3×20ms stereo frames, comfortably above the 2048-frame max quantum
/// so a single hardware pull can never empty the ring before the mixer refills.
pub const PLAYBACK_TARGET_SAMPLES: usize = 2880 * PLAYBACK_CHANNELS;
#[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>>, target_node: Option<String>) -> Result<(), AudioError>;
/// Starts playing back raw PCM audio to the output device (speaker),
/// reading mixed/incoming chunks of samples from the provided Receiver.
///
/// `ring_fill` is updated with the playback ring's current occupancy (in
/// samples) as the device drains and the worker fills it. The caller (the
/// mixer) reads it to pace production to the hardware clock — produce only
/// while the ring is below [`PLAYBACK_TARGET_SAMPLES`] — instead of on a
/// fixed timer that beats against the device quantum.
fn start_playback(
&self,
rx: Receiver<Vec<i16>>,
target_node: Option<String>,
ring_fill: Arc<AtomicUsize>,
) -> Result<(), AudioError>;
/// Stops both capture and playback streams.
fn stop(&self) -> Result<(), AudioError>;
}
pub mod clip_player;
pub mod eq;
pub mod gate;
pub mod limiter;
pub mod multitrack;
pub mod pan;
// Linear resamplers used by the Windows/cpal backend (W4). Platform-neutral and
// pure, so it builds (and its tests run) everywhere even though only the cpal
// backend wires it in.
pub mod resample;
#[cfg(target_os = "linux")]
pub mod echo_cancel;
#[cfg(target_os = "linux")]
pub mod pipewire_impl;
#[cfg(windows)]
pub mod cpal_impl;
#[cfg(target_os = "linux")]
pub mod pw_cli;
pub mod recorder;
/// A selectable audio device for the input/output pickers. `name` is the stable
/// identifier the backend uses to request the device (`target_node`);
/// `description` is the human-facing label shown in the UI. The two may be equal
/// (cpal/WASAPI) or differ (PipeWire node name vs. description).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AudioDevice {
pub name: String,
pub description: String,
pub is_input: bool,
}
impl std::fmt::Display for AudioDevice {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.description)
}
}
// Enumerate audio input/output devices for the pickers (sorted by description),
// returning the same `AudioDevice` shape regardless of platform: PipeWire
// (`pw-cli`) on Linux, cpal/WASAPI on Windows.
#[cfg(target_os = "linux")]
pub use pw_cli::enumerate_audio_devices;
#[cfg(windows)]
pub use cpal_impl::enumerate_audio_devices;
/// The audio backend implementation for the current platform.
///
/// The whole app constructs and threads this alias (via
/// `PlatformAudioBackend::new()`) rather than any concrete backend type, so
/// platform selection lives entirely here. Both implementations satisfy the
/// [`AudioBackend`] trait, which is the only interface the core talks to.
///
/// - Linux → PipeWire ([`pipewire_impl::PipeWireBackend`]).
/// - Windows → cpal/WASAPI ([`cpal_impl::CpalBackend`]).
#[cfg(target_os = "linux")]
pub type PlatformAudioBackend = pipewire_impl::PipeWireBackend;
#[cfg(windows)]
pub type PlatformAudioBackend = cpal_impl::CpalBackend;