Files
peerspeak/src/audio/mod.rs
T
molluskandClaude Opus 4.8 ddb3d2aabc Windows compat quick wins: echo-cancel UI gate, pixelpass .exe, cfg tighten (W5/W6/W9)
Three Windows-compatibility fixes from the Codex review. Implemented by Codex
(gpt-5.5); reviewed and committed by Claude.

W5 — echo cancellation is a Linux/PipeWire feature, but the toggle was shown and
live on Windows, so a Windows join tried `pactl` and errored before falling back.
Now `#[cfg(target_os = "linux")]` gates the core enable path (and the
ActiveSession guard field); on other targets the Settings + in-call controls
render as a disabled checkbox with a "not available on Windows yet" note.

W6 — pixelpass PATH lookup only tried `pixelpass`; on Windows it now also tries
`pixelpass.exe` via a cfg-selected candidate list (+ unit test).

W9 — the Linux audio stack (pipewire/pw_cli/echo_cancel/audio_probe + the
`PlatformAudioBackend` alias and device-enum re-export) was gated `cfg(unix)`;
tightened to `cfg(target_os = "linux")` so a hypothetical macOS build won't try
to compile PipeWire. cpal stays `cfg(windows)`. Genuinely-Unix file/key
permission code in lib.rs/identity.rs left as `cfg(unix)`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 04:03:49 -04:00

112 lines
4.2 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 eq;
pub mod gate;
pub mod limiter;
pub mod multitrack;
pub mod pan;
#[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;