From 2937e5191ae2b24f43bdd1ccc61fd9ccc2a4c0b1 Mon Sep 17 00:00:00 2001 From: Mollusk Date: Thu, 18 Jun 2026 17:28:12 -0400 Subject: [PATCH] Windows port Phase 2: cpal device enumeration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Give the Windows device pickers a real device list (Phase 0/1 left pw_cli returning nothing off-Linux) and generalize enumeration into a platform-neutral interface. - audio/mod.rs: move AudioDevice here (neutral home), gate pw_cli to cfg(unix), and re-export enumerate_audio_devices per-platform (pw_cli on unix, cpal_impl on windows). Also drop a now-stale "no-op stub" doc note. - cpal_impl.rs: add enumerate_audio_devices() — iterate the cpal host's input + output devices into AudioDevice (name == description == the cpal friendly name, which is what resolve() matches target_node against, so a saved selection round-trips), sorted by description. - pw_cli.rs: use super::AudioDevice instead of a local copy; parsing + tests unchanged. - app/mod.rs: one-line import change; the device-picker logic is untouched. Verified: shipped Linux state green (build --locked, clippy, 316/316, pw_cli parse tests 6/6); the cpal enumerator compiles against real cpal via the Linux/ALSA toggle. Runtime device listing on Windows is pending a real host (M2/M3). WASAPI names are less stable than PipeWire node names, so a saved device may not always round-trip (falls back to default). Co-Authored-By: Claude Opus 4.8 --- src/app/mod.rs | 2 +- src/audio/cpal_impl.rs | 45 +++++++++++++++++++++++++++++++++++++++++- src/audio/mod.rs | 29 +++++++++++++++++++++++++-- src/audio/pw_cli.rs | 14 +------------ 4 files changed, 73 insertions(+), 17 deletions(-) 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")