117 lines
4.4 KiB
Rust
117 lines
4.4 KiB
Rust
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;
|