From eda5830bf17d8e8f7bcc548ebc55791a7aeec5c4 Mon Sep 17 00:00:00 2001 From: GalaxyRuler Date: Mon, 13 Jul 2026 07:47:12 +0300 Subject: [PATCH] fix(audio): resolve specific device names instead of generic driver class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Windows mic/speaker dropdowns were showing every device as an identical generic label ("Microphone", "Microphone", "Microphone") after the cpal 0.17 migration (ac5651dc, this session's Phase 4 stable-device-ID work). Root cause: cpal 0.17.1's WASAPI backend sets description().name() from DEVPKEY_Device_DeviceDesc, the driver-class label shared by every input device using the same driver (commonly literally "Microphone"). The per-endpoint specific name (DEVPKEY_Device_FriendlyName, e.g. "Microphone (Realtek(R) Audio)") is only preserved as the first description.extended() line, and only when it differs from the generic name. Previously device_name() read only description.name(), discarding the specific name entirely. cpal 0.16's Device::name() (removed in 0.17) returned the fuller representation directly, which is why this wasn't visible before the migration; device_names_match()'s own doc comment (e3df6f8b) already notes cpal 0.17 gives "the short" description vs 0.16's longer legacy one, but only handled it for stored-selection matching, not for what gets displayed. Fix: prefer the extended() line when present, falling back to name(). Confirmed via cpal's WASAPI source (device_description.rs) that extended() carries FriendlyName exactly in the case that triggers this bug. The existing device_names_match() bidirectional short/long matching already reconciles this with any previously-stored device selections, so no migration is needed for users who already picked a device under the generic name. Verification: compiles clean (cargo build), zero clippy findings in the changed file. Could not execute `cargo test` locally — this dev machine has Windows' own System32\onnxruntime.dll, which the ort/DirectML runtime dependency (transcribe-rs's ort-directml feature) collides with at process load time (STATUS_ENTRYPOINT_NOT_FOUND), unrelated to this change and absent on CI's clean Windows runners where rust-tests has passed on every PR this session. The two new unit tests were traced by hand against DeviceDescriptionBuilder's documented behavior; CI is the actual gate. Co-Authored-By: Claude Sonnet 5 --- src-tauri/src/audio_toolkit/audio/device.rs | 45 ++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/audio_toolkit/audio/device.rs b/src-tauri/src/audio_toolkit/audio/device.rs index 30664985..500f646b 100644 --- a/src-tauri/src/audio_toolkit/audio/device.rs +++ b/src-tauri/src/audio_toolkit/audio/device.rs @@ -22,10 +22,53 @@ pub fn device_names_match(first_name: &str, second_name: &str) -> bool { || has_legacy_driver_suffix(second_name, first_name) } +/// Picks the most specific name available from a cpal device description. +/// +/// On Windows, cpal's WASAPI backend sets `description.name()` from the +/// endpoint's `DEVPKEY_Device_DeviceDesc` property, which is the generic +/// driver-class label (e.g. "Microphone") shared by every input device using +/// the same driver. When the more specific `DEVPKEY_Device_FriendlyName` +/// (e.g. "Microphone (Realtek(R) Audio)") differs, cpal stashes it as the +/// first `extended()` line instead of using it for `name()`. Prefer that +/// line so devices sharing a driver don't all enumerate under one identical +/// generic label. +fn preferred_device_name(description: &cpal::DeviceDescription) -> String { + description + .extended() + .first() + .cloned() + .unwrap_or_else(|| description.name().to_string()) +} + fn device_name(device: &cpal::Device) -> Result { device .description() - .map(|description| description.name().to_string()) + .map(|description| preferred_device_name(&description)) +} + +#[cfg(test)] +mod tests { + use super::*; + use cpal::DeviceDescriptionBuilder; + + #[test] + fn prefers_specific_friendly_name_over_generic_device_class_description() { + let description = DeviceDescriptionBuilder::new("Microphone") + .add_extended_line("Microphone (Realtek(R) Audio)") + .build(); + + assert_eq!( + preferred_device_name(&description), + "Microphone (Realtek(R) Audio)" + ); + } + + #[test] + fn falls_back_to_generic_name_when_no_extended_line_present() { + let description = DeviceDescriptionBuilder::new("USB Audio Device").build(); + + assert_eq!(preferred_device_name(&description), "USB Audio Device"); + } } pub fn list_input_devices() -> Result, Box> {