Skip to content

feat(settings): add microphone device selection#306

Merged
appergb merged 1 commit into
Open-Less:mainfrom
sora520:feat/microphone-device-selection
May 7, 2026
Merged

feat(settings): add microphone device selection#306
appergb merged 1 commit into
Open-Less:mainfrom
sora520:feat/microphone-device-selection

Conversation

@sora520
Copy link
Copy Markdown
Contributor

@sora520 sora520 commented May 6, 2026

User description

变更范围:

  • 增加麦克风设备枚举、选择和持久化相关 IPC / 状态。
  • 设置页增加首选麦克风选择 UI。
  • 首选设备不可用时临时回退系统默认麦克风,设备恢复后自动回到首选设备。
  • 同步更新 zh-CN / zh-TW / en / ja / ko 文案。
  • 补充托盘麦克风菜单刷新,设备变化时自动更新托盘菜单,不增加手动刷新菜单项。

Review 后补充修复:

  • 修复设置页麦克风电平预览 monitor 的异步 cleanup / stop-start 竞态。
  • 将预览 recorder start / stop 放到 blocking 线程,避免设备初始化或释放较慢时阻塞 UI。
  • 听写 / QA recorder 启动前停止设置页预览 recorder,避免同一输入设备被并发占用。

平台验证:

  • Windows 11:
    • npm run build 通过。
    • cargo check --manifest-path src-tauri/Cargo.toml 通过。
    • 已手动验证麦克风选择、断开后回退系统默认、重连后恢复首选设备。
  • macOS:
    • 当前实现基于 cpal 跨平台设备枚举,理论上可用。
    • 尚未在 macOS 真机验证,需要后续补测。
  • Linux:
    • 当前实现基于 cpal 跨平台设备枚举,理论上可用。
    • 实际行为依赖 ALSA / PulseAudio / PipeWire 配置,尚未真机验证。

实现说明:

  • 当前麦克风选择按 cpal 暴露的设备名称保存和匹配,保持跨平台实现简单,并兼容现有偏好数据。
  • 使用平台稳定设备 ID 会是更完整的方案,能区分同名设备,也能在重启 / 重连后更准确地恢复到同一物理设备。
  • 本 PR 没有切到稳定设备 ID,因为 cpal 的跨平台公共 API 没有统一暴露稳定 ID;完整实现需要分别接入 Windows endpoint ID / macOS CoreAudio UID,并设计旧偏好的兼容迁移,适合后续单独处理。

PR Type

Enhancement


Description

  • Add microphone device selection to settings and tray

    • Enumerate devices via cpal, store preference, fallback to default
    • Use selected device for dictation/QA recording
  • Implement live level monitor in settings picker dialog

    • Start/stop level monitor via blocking IPC commands
    • Stop preview before recording to avoid device contention
  • Integrate tray microphone menu with auto-refresh

    • Build menu on startup, update on device changes
    • Sync selection with settings and prefs:changed event
  • Update i18n strings for English, Japanese, Korean, Chinese


Diagram Walkthrough

flowchart LR
  A["Settings UI / Tray"] -- "select device" --> B["UserPreferences"]
  B -- "prefs:changed event" --> C["HotkeySettingsContext sync"]
  D["Coordinator (dictation/QA)"] -- "read pref" --> E["Recorder::start(device)"]
  E -- "select or fallback" --> F["Audio capture"]
  G["Tray watcher"] -- "device changes" --> H["Refresh tray menu"]
  H -- "devices-changed event" --> A
  I["Level monitor in picker"] -- "start/stop IPC" --> E
  E -- "level events" --> I
Loading

File Walkthrough

Relevant files
Enhancement
14 files
commands.rs
Add microphone listing, level monitor start/stop commands, and tray
state sync
+85/-9   
coordinator.rs
Integrate microphone device selection and preview stop into recording
pipeline
+27/-2   
lib.rs
Build tray microphone menu, background device watcher, and event
handling
+196/-14
recorder.rs
Add device enumeration and selectable input device parameter
+79/-13 
types.rs
Add microphone_device_name field to UserPreferences and wire type
+8/-0     
en.ts
Add English microphone UI localization strings                     
+10/-0   
ja.ts
Add Japanese microphone UI localization strings                   
+10/-0   
ko.ts
Add Korean microphone UI localization strings                       
+10/-0   
zh-CN.ts
Add Chinese microphone UI localization strings                     
+11/-1   
zh-TW.ts
Add Chinese (Taiwan) microphone UI localization strings   
+10/-0   
ipc.ts
Add frontend IPC wrappers and mock data for microphone features
+19/-0   
types.ts
Add MicrophoneDevice interface and microphoneDeviceName to
UserPreferences
+7/-0     
Settings.tsx
Add microphone picker dialog with live level meter and monitor control
+472/-5 
HotkeySettingsContext.tsx
Add prefs:changed event listener for cross-component microphone sync
+29/-1   

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 6, 2026

PR Reviewer Guide 🔍

(Review updated until commit 98e3d37)

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Stale reload

loadMicrophoneDevices has no request versioning, so a slower initial load or manual refresh can complete after a later device-change refresh and overwrite the newer device list. If devices are hot-plugged quickly, the picker can temporarily show stale entries until another refresh happens.

const loadMicrophoneDevices = useCallback(async (
  signal?: { cancelled: boolean },
  options: { showLoading?: boolean } = {},
) => {
  if (options.showLoading ?? true) {
    setMicrophoneDevicesLoaded(false);
  }
  setMicrophoneDevicesError(null);
  try {
    const devices = await listMicrophoneDevices();
    if (signal?.cancelled) return;
    setMicrophoneDevices(devices);
    setMicrophoneDevicesLoaded(true);
  } catch (err) {
    console.error('[settings] list microphone devices failed', err);
    if (signal?.cancelled) return;
    setMicrophoneDevices([]);
    setMicrophoneDevicesError(err instanceof Error ? err.message : String(err));
    setMicrophoneDevicesLoaded(true);
  }
Device identity

The selected microphone is persisted and resolved by display name only. On machines with duplicate names, or when a device is replaced by another with the same name, Recorder::start can reopen the wrong physical input after reconnect or restart.

fn select_input_device(
    host: &cpal::Host,
    microphone_device_name: Option<&str>,
) -> Result<cpal::Device, RecorderError> {
    let preferred = microphone_device_name
        .map(str::trim)
        .filter(|name| !name.is_empty());
    if let Some(preferred) = preferred {
        let devices = host
            .input_devices()
            .map_err(|e| RecorderError::EngineFailed(format!("input_devices: {e}")))?;
        for device in devices {
            if device.name().ok().as_deref() == Some(preferred) {
                return Ok(device);
            }
        }
        log::warn!(
            "[recorder] preferred input device not found; falling back to default: {preferred}"
        );
    }

    host.default_input_device()
        .ok_or_else(|| RecorderError::EngineFailed("no default input device".into()))

@sora520 sora520 force-pushed the feat/microphone-device-selection branch from c24c72c to be9daa1 Compare May 7, 2026 02:24
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 7, 2026

Persistent review updated to latest commit be9daa1

@appergb
Copy link
Copy Markdown
Collaborator

appergb commented May 7, 2026

@claude 这个问题是否确实存在?是否已经解决?是否代码逻辑质量结构清晰完整?如果可以,请告诉我。

@claude
Copy link
Copy Markdown

claude Bot commented May 7, 2026

Claude finished @appergb's task in 1m 52s —— View job


代码审查结果

  • 理解请求
  • 读取核心 Rust 文件(recorder.rs, commands.rs, types.rs, coordinator.rs, lib.rs)
  • 读取前端文件(Settings.tsx)
  • 分析重复设备名称问题是否真实存在及是否已解决
  • 评估整体代码逻辑质量与结构完整性

1. 重复设备名称问题:确实存在,本 PR 未解决,属于已知且有意推迟的设计决策

问题是否真实存在?
是。在 recorder.rs:121-143list_input_devices 中:

result.push(MicrophoneDevice {
    id: name.clone(),   // ← id 只是 name 的副本,不是稳定平台 ID
    name,
    is_default: ...,
});

id 字段与 name 完全相同。select_input_devicerecorder.rs:290-313)按名称字符串匹配设备:

if device.name().ok().as_deref() == Some(preferred) {
    return Ok(device);  // 同名时取第一个,无法区分
}

如果有两个同名设备(常见场景:两个相同型号的 USB 耳麦,或"USB Audio"/"Headset Microphone"等通用名),选择行为是不确定的——总是匹配到枚举顺序中第一个同名设备。

是否已解决?
否。但 PR 描述中明确承认并说明了原因

"使用平台稳定设备 ID 会是更完整的方案,能区分同名设备……本 PR 没有切到稳定设备 ID,因为 cpal 的跨平台公共 API 没有统一暴露稳定 ID;完整实现需要分别接入 Windows endpoint ID / macOS CoreAudio UID。"

这是一个有充分理由的有意推迟,并非遗漏。对于大多数用户(单一设备或设备名唯一)不构成实际问题。


2. 代码逻辑质量和结构完整性评估

做得好的地方:

  • 并发竞态修复正确MicrophonePickerDialogenqueueMonitorTask 串行化了 stop/start 操作,避免了异步 cleanup 竞态(Settings.tsx:444-448)。
  • Blocking 线程正确start_microphone_level_monitorstop_microphone_level_monitor 都用 spawn_blockingcommands.rs:186, 215),避免阻塞 Tauri async 运行时。
  • 听写前停止预览coordinator.rsstop_microphone_preview_monitor 在听写录音和 QA 录音启动前分别调用(行 2118, 3410),避免设备独占冲突。
  • 回退逻辑健壮:首选设备不可用时静默回退系统默认,符合 CLAUDE.md 的"silent fallback"约定。
  • 托盘监听线程:用设备签名变化检测触发刷新,不无谓重刷(lib.rs:421-434)。
  • 整体架构合规:新 IPC 命令在 commands.rs,状态机变更在 coordinator.rs,叶模块不跨模块调用,符合 CLAUDE.md 约定。

可以改进的地方:

(偏轻量,不阻 merge)

  1. MicrophoneDevice.id 命名误导id 的语义通常是稳定的不透明标识符,但这里是 name.clone()recorder.rs:139)。前端使用 ${device.id}-${index} 作为 React key 时需要 index 补位才能区分同名设备,说明 id 本身没有提供唯一性。可考虑把字段重命名为 name only,不暴露误导性的 id,或在注释里明确说明限制。

  2. Settings 页 2 秒轮询RecordingSection 每 2 秒调用一次 listMicrophoneDevicesSettings.tsx:217-222),即使页面打开着但什么都没变化,也持续产生 IPC 流量。托盘侧已经有设备变化监听线程(lib.rs:436),可考虑通过事件通知前端,替代轮询。

  3. 托盘 watcher 线程无退出机制start_tray_microphone_watcher 线程是永久循环(lib.rs:440-458),没有与进程退出挂钩的停止路径。这是轻量后台线程,但从优雅退出的角度,可以在 RunEvent::Exit 中用 AtomicBool 通知停止。

  4. SettingsWriter 实现重复commands.rsCoordinatorArc<Coordinator> 各自实现了 SettingsWriter(行 61-119),Arc 实现完全 delegate 到内层,属于重复样板代码。

(需确认)

  1. refresh_tray_microphone_menu 整体重建代价:每次设备变化时重建整个托盘菜单(lib.rs:410-418),在某些平台(Windows)可能有短暂闪烁,但 Tauri 托盘更新通常是异步的,实际影响可能可以接受。

总结

该问题(重复设备名称)确实存在,本 PR 有意未解决,并在 PR 描述中诚实说明了原因和后续路径。这个决策是合理的。

整体代码结构清晰,并发问题修复正确,IPC/状态/前端分层符合项目约定。上述可改进点均为轻量性建议,不影响功能正确性。可以 merge,后续单独跟进稳定设备 ID 支持。

@appergb
Copy link
Copy Markdown
Collaborator

appergb commented May 7, 2026

修复 claude提出的这些问题,随后经过审查便可合并了。

@sora520 sora520 force-pushed the feat/microphone-device-selection branch from be9daa1 to 98e3d37 Compare May 7, 2026 03:39
Copy link
Copy Markdown
Contributor Author

sora520 commented May 7, 2026

已处理 Claude 提到的轻量改进项:

  • 移除 MicrophoneDevice.id 字段,避免把设备名称伪装成稳定 ID;前端列表 key 改为 name + index
  • 设置页不再每 2 秒轮询 listMicrophoneDevices,改为监听后端 microphone:devices-changed 事件刷新。
  • 托盘麦克风 watcher 增加 RunEvent::Exit 退出信号,进程退出时停止循环。
  • SettingsWriterArc<Coordinator> 重复实现改为通用 impl<T: SettingsWriter + ?Sized> SettingsWriter for Arc<T>

关于 refresh_tray_microphone_menu 整体重建:已评估,当前只在设备签名变化时触发,不是固定频率刷新;麦克风插拔 / 蓝牙连接属于低频事件。Tauri 托盘整体 set_menu 是现有实现方式,改成局部更新会引入更多菜单对象状态和平台差异。该点先保持现状,如果后续发现 Windows/macOS 上有明显闪烁或交互中断,再单独改为局部更新。

验证:

  • npm run build 通过。
  • cargo check --manifest-path src-tauri/Cargo.toml 通过。

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 7, 2026

Persistent review updated to latest commit 98e3d37

@appergb appergb merged commit ec773f0 into Open-Less:main May 7, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants