diff --git a/src/app/mod.rs b/src/app/mod.rs index 928157e..4eeee53 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -2,7 +2,7 @@ use crate::core::{CoreController, messages::{CoreCommand, UiEvent}}; use crate::network::PeerState; use crate::notify::{self, Sound}; use crate::audio::eq::{EqSettings, EQ_GAIN_DB_MAX, EQ_GAIN_DB_MIN}; -use crate::audio::pw_cli::{AudioDevice, enumerate_audio_devices}; +use crate::audio::{AudioDevice, enumerate_audio_devices}; use crate::config::{AppConfig, NetworkMode, RecordingMode, RoomLayout}; use crate::hotkeys::{format_binding, HotkeyAction, HotkeyContext, KeyBinding}; use crate::presence::PresenceMode; diff --git a/src/audio/cpal_impl.rs b/src/audio/cpal_impl.rs index 4555a97..1a042d0 100644 --- a/src/audio/cpal_impl.rs +++ b/src/audio/cpal_impl.rs @@ -42,7 +42,7 @@ use ringbuf::{ HeapRb, }; -use super::{AudioBackend, AudioError, PLAYBACK_CHANNELS, PLAYBACK_TARGET_SAMPLES}; +use super::{AudioBackend, AudioDevice, AudioError, PLAYBACK_CHANNELS, PLAYBACK_TARGET_SAMPLES}; /// The one sample rate the pipeline supports (Opus + the 20 ms frame). const SAMPLE_RATE: u32 = 48_000; @@ -143,6 +143,49 @@ impl AudioBackend for CpalBackend { } } +// --------------------------------------------------------------------------- +// Device enumeration (for the settings device pickers) +// --------------------------------------------------------------------------- + +/// Enumerate WASAPI input/output devices via cpal, sorted by description to match +/// the PipeWire backend's stable UI ordering. +/// +/// cpal exposes a single friendly name per device, which is also what [`resolve`] +/// matches `target_node` against — so `name` and `description` are the same string +/// and a saved selection round-trips. Note: WASAPI device names are less stable +/// across driver/endpoint changes than PipeWire node names, so a saved device may +/// not always be found again; selection then falls back to the system default. +pub fn enumerate_audio_devices() -> Vec { + let host = cpal::default_host(); + let mut devices = Vec::new(); + + if let Ok(inputs) = host.input_devices() { + for device in inputs { + if let Ok(name) = device.name() { + devices.push(AudioDevice { + description: name.clone(), + name, + is_input: true, + }); + } + } + } + if let Ok(outputs) = host.output_devices() { + for device in outputs { + if let Ok(name) = device.name() { + devices.push(AudioDevice { + description: name.clone(), + name, + is_input: false, + }); + } + } + } + + devices.sort_by(|a, b| a.description.cmp(&b.description)); + devices +} + // --------------------------------------------------------------------------- // Device / config selection // --------------------------------------------------------------------------- diff --git a/src/audio/mod.rs b/src/audio/mod.rs index c601a36..29eb492 100644 --- a/src/audio/mod.rs +++ b/src/audio/mod.rs @@ -66,9 +66,35 @@ pub mod pan; pub mod pipewire_impl; #[cfg(windows)] pub mod cpal_impl; +#[cfg(unix)] 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(unix)] +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 @@ -77,8 +103,7 @@ pub mod recorder; /// [`AudioBackend`] trait, which is the only interface the core talks to. /// /// - Linux/Unix → PipeWire ([`pipewire_impl::PipeWireBackend`]). -/// - Windows → cpal/WASAPI ([`cpal_impl::CpalBackend`]); a no-op stub until the -/// Phase 1 capture/playback implementation lands. +/// - Windows → cpal/WASAPI ([`cpal_impl::CpalBackend`]). #[cfg(unix)] pub type PlatformAudioBackend = pipewire_impl::PipeWireBackend; #[cfg(windows)] diff --git a/src/audio/pw_cli.rs b/src/audio/pw_cli.rs index 51d3ed6..f1e4e61 100644 --- a/src/audio/pw_cli.rs +++ b/src/audio/pw_cli.rs @@ -1,18 +1,6 @@ +use super::AudioDevice; use std::process::Command; -#[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) - } -} - pub fn enumerate_audio_devices() -> Vec { let output = Command::new("pw-cli") .arg("list-objects")