Skip to content

mic selector#1060

Closed
plyght wants to merge 13 commits intofastrepl:mainfrom
plyght:plyght/mic-selector
Closed

mic selector#1060
plyght wants to merge 13 commits intofastrepl:mainfrom
plyght:plyght/mic-selector

Conversation

@plyght
Copy link
Contributor

@plyght plyght commented Jul 3, 2025

No description provided.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jul 3, 2025

📝 Walkthrough

Walkthrough

This change implements comprehensive microphone device selection and management across the application. It introduces new UI controls for selecting microphones, updates plugin commands to get and set the selected device, adds device validation and switching support in the backend, and synchronizes configuration and localization to support this feature.

Changes

Files/Paths Change Summary
apps/desktop/src/components/editor-area/note-header/listen-button.tsx Added MicControlWithDropdown component for microphone device selection and integrated it into AudioControlButton.
apps/desktop/src/components/settings/views/general.tsx
apps/desktop/src/components/settings/views/sound.tsx
Updated settings logic to include microphone device selection, React Query hooks, and device selection UI.
apps/desktop/src/locales/en/messages.po
apps/desktop/src/locales/ko/messages.po
Added and updated translation keys and source references for microphone device selection and sound settings.
crates/audio/Cargo.toml
plugins/listener/Cargo.toml
Updated dependencies for audio device management and UUID support.
crates/audio/src/errors.rs
crates/audio/src/lib.rs
crates/audio/src/mic.rs
Added AudioError enum, new exports, and a custom microphone input/stream implementation with device selection and validation.
crates/db-user/src/config_types.rs
plugins/db/js/bindings.gen.ts
Renamed/added selected_microphone_device field in configuration types.
plugins/listener/build.rs
plugins/listener/js/bindings.gen.ts
Added new plugin commands for getting/setting the selected microphone device and updated JS bindings.
plugins/listener/permissions/autogenerated/commands/get_selected_microphone_device.toml
plugins/listener/permissions/autogenerated/commands/set_selected_microphone_device.toml
plugins/listener/permissions/default.toml
plugins/listener/permissions/schemas/schema.json
plugins/listener/permissions/autogenerated/reference.md
Added permissions and documentation for new microphone device commands.
plugins/listener/src/commands.rs
plugins/listener/src/ext.rs
plugins/listener/src/fsm.rs
plugins/listener/src/error.rs
Refactored device listing command into get/set device commands, added dynamic device switching, and extended error handling.
plugins/listener/src/lib.rs Registered new get/set microphone device commands in the plugin interface.
plugins/db/src/ext.rs Added db_set_config async method to update user configuration in the database.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI (MicControlWithDropdown)
    participant Plugin (listener)
    participant Config DB
    participant Audio Layer

    User->>UI (MicControlWithDropdown): Opens mic device dropdown
    UI->>Plugin (listener): getSelectedMicrophoneDevice()
    Plugin (listener)->>Config DB: Read selected_microphone_device
    Plugin (listener)->>Audio Layer: List available devices
    Plugin (listener)-->>UI: Return device list + current selection

    User->>UI (MicControlWithDropdown): Selects device
    UI->>Plugin (listener): setSelectedMicrophoneDevice(deviceName)
    Plugin (listener)->>Audio Layer: Validate deviceName
    Plugin (listener)->>Config DB: Update selected_microphone_device
    Plugin (listener)->>Audio Layer: (If session active) Switch device live
    Plugin (listener)-->>UI: Confirm update
Loading

Possibly related issues

  • Microphone input selector #965: Implements the microphone input selector feature with UI, plugin commands, state machine, and audio input handling, directly matching this PR's scope.

Possibly related PRs

  • Overlay floating control #908: Also modifies listen-button.tsx for microphone control, but focuses on mute state management, not device selection; both PRs alter the same area but address different aspects.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Clippy (1.86.0)
Updating git repository `https://github.com/RustAudio/cpal`

error: failed to load source for dependency cpal

Caused by:
Unable to update https://github.com/RustAudio/cpal?rev=51c3b43#51c3b43c

Caused by:
failed to create directory /usr/local/git/db/cpal-476cd1dd23dbc279

Caused by:
Permission denied (os error 13)


📜 Recent review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3688fd6 and fa32f6f.

📒 Files selected for processing (8)
  • apps/desktop/src/components/editor-area/note-header/listen-button.tsx (4 hunks)
  • apps/desktop/src/locales/en/messages.po (32 hunks)
  • apps/desktop/src/locales/ko/messages.po (32 hunks)
  • apps/docs/data/i18n.json (1 hunks)
  • crates/audio/src/lib.rs (3 hunks)
  • crates/audio/src/mic.rs (2 hunks)
  • plugins/db/src/ext.rs (2 hunks)
  • plugins/listener/src/ext.rs (5 hunks)
✅ Files skipped from review due to trivial changes (2)
  • apps/docs/data/i18n.json
  • apps/desktop/src/locales/ko/messages.po
🚧 Files skipped from review as they are similar to previous changes (3)
  • plugins/listener/src/ext.rs
  • apps/desktop/src/locales/en/messages.po
  • crates/audio/src/lib.rs
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.{js,ts,tsx,rs}`: 1. No error handling. 2. No unused imports, variables, or functions. 3. For comments, keep it minimal. It should be about "Why", not "What".

**/*.{js,ts,tsx,rs}: 1. No error handling.
2. No unused imports, variables, or functions.
3. For comments, keep it minimal. It should be about "Why", not "What".

⚙️ Source: CodeRabbit Configuration File

List of files the instruction was applied to:

  • plugins/db/src/ext.rs
  • apps/desktop/src/components/editor-area/note-header/listen-button.tsx
  • crates/audio/src/mic.rs
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: ci (windows, windows-latest)
  • GitHub Check: ci (macos, macos-latest)
  • GitHub Check: ci
🔇 Additional comments (8)
plugins/db/src/ext.rs (2)

20-23: LGTM! Method signature follows established patterns.

The new db_set_config method maintains consistency with other methods in the trait, using the same async signature pattern and error handling approach.


139-146: LGTM! Implementation follows established patterns.

The implementation correctly follows the same pattern as other database operations: acquire state lock, get database reference, check for initialization, and call the underlying database method with proper error propagation.

apps/desktop/src/components/editor-area/note-header/listen-button.tsx (4)

1-1: Import additions look good.

The new imports for Trans, useLingui, and sonnerToast are appropriately added to support the enhanced microphone control functionality.

Also applies to: 24-24


322-324: Good refactoring to support device selection.

The conditional logic properly delegates to the new MicControlWithDropdown component for microphone controls while maintaining the existing behavior for speaker controls.


366-391: Clean component structure with proper permission checks.

The MicControlWithDropdown component is well-structured with appropriate React Query hooks for data fetching and proper conditional rendering based on microphone permissions.


501-513: Good error state handling.

The UI properly displays loading states and error messages when device enumeration fails, providing clear feedback to users about permission or system issues.

crates/audio/src/mic.rs (2)

138-200: Well-structured stream building with multiple format support.

The implementation provides comprehensive support for different sample formats (F32, I16, U16) with proper error handling for unsupported formats.


234-307: Efficient audio processing with proper normalization.

The sample processing functions properly handle multi-channel to mono conversion and sample rate resampling while normalizing different sample formats to f32 range.

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

‼️ IMPORTANT
Auto-reply has been disabled for this repository in the CodeRabbit settings. The CodeRabbit bot will not respond to your replies unless it is explicitly tagged.

  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🧹 Nitpick comments (7)
apps/desktop/src/locales/ko/messages.po (1)

302-302: Korean translations needed for new microphone device selection features

Multiple new message entries related to microphone device selection have been added but remain untranslated (empty msgstr ""). These include device loading states, selection prompts, and access descriptions.

Consider getting Korean translations for the new microphone device selection features to maintain localization completeness:

  • "Loading available devices..."
  • "Microphone Device"
  • "Select microphone device"
  • "System Default"
  • And other related entries

Also applies to: 546-546, 715-717, 723-725, 761-763, 765-765, 769-771, 923-923, 927-927, 931-931, 985-987, 993-995, 1053-1053, 1057-1060

crates/audio/src/errors.rs (1)

25-27: Remove unused empty Error enum.

This empty enum appears to be unused and should be removed to avoid confusion.

-#[derive(thiserror::Error, Debug)]
-pub enum Error {}
-
apps/desktop/src/components/settings/views/sound.tsx (2)

84-92: Remove console.log statements before production.

Debug logging should be removed or replaced with proper error tracking.

-      console.log("Attempting to call getSelectedMicrophoneDevice...");
       try {
         const result = await listenerCommands.getSelectedMicrophoneDevice();
-        console.log("Device query result:", result);
         return result;
       } catch (error) {
-        console.error("Device query failed:", error);
         throw error;
       }

Also applies to: 101-101, 104-104, 106-106


102-102: Use optional chaining for cleaner code.

-      if (result && result.startsWith("DEVICES:")) {
+      if (result?.startsWith("DEVICES:")) {

Also applies to: 118-118

apps/desktop/src/components/editor-area/note-header/listen-button.tsx (1)

383-383: Use optional chaining as suggested by static analysis.

-          if (parsedData && typeof parsedData === 'object' && parsedData.devices) {
+          if (parsedData?.devices) {
-          if (parsedData && typeof parsedData === 'object' && parsedData.selected) {
+          if (parsedData?.selected) {

Also applies to: 409-409

crates/audio/src/mic.rs (2)

136-136: Use proper logging in error callbacks.

-        |err| eprintln!("Audio stream error: {}", err),
+        |err| tracing::error!("Audio stream error: {}", err),

Also applies to: 168-168, 201-201


101-102: Consider making sample rate configurable.

The hardcoded 16kHz sample rate might not be optimal for all use cases. Some devices or speech recognition systems might work better with different rates.

Consider adding a parameter or configuration option:

-    pub fn sample_rate(&self) -> u32 {
-        16000 // Standard sample rate for speech
+    pub fn sample_rate(&self) -> u32 {
+        self.target_sample_rate.unwrap_or(16000) // Default to 16kHz for speech
     }

Also applies to: 344-344, 378-379

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7b170c9 and 3671916.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (24)
  • apps/desktop/src/components/editor-area/note-header/listen-button.tsx (3 hunks)
  • apps/desktop/src/components/settings/views/general.tsx (1 hunks)
  • apps/desktop/src/components/settings/views/sound.tsx (2 hunks)
  • apps/desktop/src/locales/en/messages.po (19 hunks)
  • apps/desktop/src/locales/ko/messages.po (19 hunks)
  • crates/audio/Cargo.toml (1 hunks)
  • crates/audio/src/errors.rs (1 hunks)
  • crates/audio/src/lib.rs (2 hunks)
  • crates/audio/src/mic.rs (1 hunks)
  • crates/db-user/src/config_types.rs (2 hunks)
  • plugins/db/js/bindings.gen.ts (1 hunks)
  • plugins/listener/Cargo.toml (1 hunks)
  • plugins/listener/build.rs (1 hunks)
  • plugins/listener/js/bindings.gen.ts (1 hunks)
  • plugins/listener/permissions/autogenerated/commands/get_selected_microphone_device.toml (1 hunks)
  • plugins/listener/permissions/autogenerated/commands/set_selected_microphone_device.toml (1 hunks)
  • plugins/listener/permissions/autogenerated/reference.md (3 hunks)
  • plugins/listener/permissions/default.toml (1 hunks)
  • plugins/listener/permissions/schemas/schema.json (3 hunks)
  • plugins/listener/src/commands.rs (2 hunks)
  • plugins/listener/src/error.rs (1 hunks)
  • plugins/listener/src/ext.rs (5 hunks)
  • plugins/listener/src/fsm.rs (5 hunks)
  • plugins/listener/src/lib.rs (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.{js,ts,tsx,rs}`: 1. No error handling. 2. No unused imports, variables, or functions. 3. For comments, keep it minimal. It should be about "Why", not "What".

**/*.{js,ts,tsx,rs}: 1. No error handling.
2. No unused imports, variables, or functions.
3. For comments, keep it minimal. It should be about "Why", not "What".

⚙️ Source: CodeRabbit Configuration File

List of files the instruction was applied to:

  • plugins/listener/build.rs
  • plugins/listener/src/error.rs
  • crates/db-user/src/config_types.rs
  • apps/desktop/src/components/settings/views/general.tsx
  • plugins/db/js/bindings.gen.ts
  • plugins/listener/js/bindings.gen.ts
  • plugins/listener/src/lib.rs
  • crates/audio/src/errors.rs
  • plugins/listener/src/ext.rs
  • plugins/listener/src/fsm.rs
  • crates/audio/src/lib.rs
  • apps/desktop/src/components/editor-area/note-header/listen-button.tsx
  • plugins/listener/src/commands.rs
  • apps/desktop/src/components/settings/views/sound.tsx
  • crates/audio/src/mic.rs
🧬 Code Graph Analysis (3)
plugins/listener/src/fsm.rs (2)
crates/audio/src/lib.rs (2)
  • from_mic_device (82-95)
  • from_mic (73-80)
plugins/listener/src/ext.rs (11)
  • state (172-172)
  • state (276-276)
  • state (283-283)
  • state (293-293)
  • state (303-303)
  • state (314-314)
  • state (325-325)
  • state (336-336)
  • state (347-347)
  • state (358-358)
  • state (369-369)
apps/desktop/src/components/editor-area/note-header/listen-button.tsx (5)
packages/ui/src/components/ui/button.tsx (1)
  • Button (37-89)
packages/ui/src/lib/utils.ts (1)
  • cn (4-6)
apps/desktop/src/components/sound-indicator.tsx (1)
  • SoundIndicator (6-31)
packages/ui/src/components/ui/popover.tsx (3)
  • Popover (85-85)
  • PopoverTrigger (85-85)
  • PopoverContent (85-85)
packages/ui/src/components/ui/spinner.tsx (1)
  • Spinner (58-58)
apps/desktop/src/components/settings/views/sound.tsx (1)
packages/ui/src/components/ui/select.tsx (5)
  • Select (174-174)
  • SelectTrigger (174-174)
  • SelectValue (174-174)
  • SelectContent (174-174)
  • SelectItem (174-174)
🪛 Biome (1.9.4)
apps/desktop/src/components/editor-area/note-header/listen-button.tsx

[error] 377-377: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 403-403: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

apps/desktop/src/components/settings/views/sound.tsx

[error] 102-102: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 118-118: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: ci (macos, macos-latest)
  • GitHub Check: ci (windows, windows-latest)
  • GitHub Check: ci
🔇 Additional comments (23)
plugins/listener/Cargo.toml (1)

55-55: LGTM! Clean dependency addition.

The uuid dependency with the v4 feature is properly added to support unique identification for microphone device management functionality.

crates/audio/Cargo.toml (1)

17-18: LGTM! Appropriate dependency enhancements.

The addition of the "sync" feature to tokio and the new tokio-stream dependency properly support the expanded asynchronous audio input handling and streaming capabilities.

plugins/listener/build.rs (1)

2-3: LGTM! Proper command registration.

The new commands get_selected_microphone_device and set_selected_microphone_device are properly added to the COMMANDS array to support microphone device selection functionality.

plugins/listener/src/error.rs (1)

19-20: LGTM! Proper error handling addition.

The NoneUser error variant is correctly implemented following the established pattern and supports user context validation for microphone device operations.

plugins/listener/permissions/default.toml (1)

4-5: LGTM! Proper permission configuration.

The new permissions allow-get-selected-microphone-device and allow-set-selected-microphone-device are correctly added to provide controlled access to the microphone device selection functionality.

plugins/listener/permissions/autogenerated/commands/get_selected_microphone_device.toml (1)

1-14: LGTM! Permission configuration follows expected pattern.

The permission definitions are correctly structured with proper schema reference and both allow/deny entries for the get_selected_microphone_device command.

apps/desktop/src/components/settings/views/general.tsx (1)

129-130: LGTM! Proper field preservation with safe access patterns.

The mutation correctly preserves the selected_microphone_device field using optional chaining and null fallback, maintaining consistency with the existing codebase pattern.

crates/db-user/src/config_types.rs (2)

47-47: LGTM! Field addition follows established patterns.

The selected_microphone_device field is properly typed as Option<String> and aligns with the microphone device selection feature.


59-59: LGTM! Default implementation correctly updated.

The Default implementation properly initializes the new field to None, maintaining consistency with other optional fields.

plugins/listener/permissions/autogenerated/commands/set_selected_microphone_device.toml (1)

1-14: LGTM! Permission configuration follows expected pattern.

The permission definitions are correctly structured with proper schema reference and both allow/deny entries for the set_selected_microphone_device command.

plugins/db/js/bindings.gen.ts (1)

154-154: LGTM! Type definition correctly reflects backend changes.

The selected_microphone_device field is properly typed as string | null to match the Rust Option<String> type, and the generated binding is consistent with the backend structure.

plugins/listener/src/lib.rs (1)

31-32: Clean API refactor for device selection confirmed

We’ve verified that both new commands are fully implemented and registered:

  • plugins/listener/src/commands.rs defines pub async fn get_selected_microphone_device and pub async fn set_selected_microphone_device
  • Extension methods exist in plugins/listener/src/ext.rs
  • Commands are registered in plugins/listener/build.rs and exposed via JS bindings and permission schemas

No further action needed—approving the changes.

crates/audio/src/lib.rs (2)

7-7: Good practice: explicit exports over wildcard

Explicitly exporting only AudioError and Error instead of using wildcard imports improves code clarity and prevents accidental exposure of internal types.


82-95: Well-implemented device-specific constructor

The new from_mic_device method properly handles device selection with appropriate fallback to default behavior. The implementation follows the existing constructor pattern and correctly initializes all fields.

plugins/listener/permissions/autogenerated/reference.md (1)

7-8: Auto-generated permissions correctly reflect new commands

The permission documentation has been properly updated to include the new microphone device selection commands with appropriate allow/deny variants following the established naming conventions.

Also applies to: 115-136, 401-422

plugins/listener/js/bindings.gen.ts (1)

10-15: TypeScript bindings correctly implement new device selection API

The generated bindings properly expose the new microphone device selection commands with appropriate TypeScript types. The string | null return type for the getter and null parameter/return types for the setter follow established conventions.

crates/audio/src/errors.rs (1)

1-23: LGTM! Well-structured error handling for audio operations.

The AudioError enum provides comprehensive coverage of audio-related errors with proper use of thiserror for automatic conversions from cpal errors.

apps/desktop/src/locales/en/messages.po (1)

1-1234: LGTM! Localization updates for microphone device selection.

The localization file properly includes all necessary translation entries for the new microphone device selection UI.

plugins/listener/permissions/schemas/schema.json (1)

333-344: LGTM! Permission schema properly extended for microphone device management.

The new permissions follow the established pattern and are correctly included in the default permission set.

Also applies to: 465-476, 514-517

apps/desktop/src/components/settings/views/sound.tsx (1)

97-112: Add error handling for JSON parsing.

The JSON.parse call could throw an exception if the response format is invalid.

     queryFn: async () => {
       const result = deviceQuery.data;
-      console.log("Processing device query result:", result);
       if (result && result.startsWith("DEVICES:")) {
         const devicesJson = result.substring(8);
-        console.log("Devices JSON:", devicesJson);
-        const devices = JSON.parse(devicesJson) as string[];
-        console.log("Parsed devices:", devices);
-        return devices;
+        try {
+          const parsed = JSON.parse(devicesJson);
+          return parsed.devices || [];
+        } catch (error) {
+          console.error("Failed to parse device JSON:", error);
+          return [];
+        }
       }
       return [];
     },

Likely an incorrect or invalid review comment.

plugins/listener/src/commands.rs (1)

41-67: LGTM! Thorough device validation with clear error messages.

The validation logic properly checks device availability before setting it, with informative error messages.

plugins/listener/src/ext.rs (1)

46-117: Excellent error handling and fallback logic!

The enhanced list_microphone_devices implementation provides robust error handling with appropriate fallbacks:

  • Gracefully handles device enumeration failures
  • Falls back to default device when enumeration fails
  • Provides detailed logging for debugging
  • Never panics and always returns a valid result

This is a significant improvement over typical device enumeration code.

plugins/listener/src/fsm.rs (1)

103-127: Good integration of device selection from config.

The implementation correctly:

  • Retrieves the selected device from user configuration
  • Falls back to default device if none selected
  • Provides clear logging for debugging
  • Removes the delay for faster device switching

The comment explaining the delay removal is helpful for future maintainers.

Comment on lines 6 to 29
pub async fn get_selected_microphone_device<R: tauri::Runtime>(
app: tauri::AppHandle<R>,
) -> Result<Vec<String>, String> {
app.list_microphone_devices()
) -> Result<Option<String>, String> {
// Get the currently selected device from config
let selected_device = app.get_selected_microphone_device()
.await
.map_err(|e| e.to_string())
.map_err(|e| e.to_string())?;

// Always return device list with selected device info
if let Ok(devices) = app.list_microphone_devices().await {
tracing::info!("Available devices: {:?}, Selected: {:?}", devices, selected_device);

// Create a combined response that includes both device list and selected device
let mut response = std::collections::HashMap::new();
response.insert("devices".to_string(), devices);
response.insert("selected".to_string(), vec![selected_device.unwrap_or_default()]);

let response_json = serde_json::to_string(&response).unwrap_or_default();
return Ok(Some(format!("DEVICES:{}", response_json)));
}

// Fallback to just selected device
Ok(selected_device)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Simplify and clarify the return format.

The current implementation mixes different response formats which is confusing. Consider returning a structured response consistently.

Consider returning a proper struct:

#[derive(serde::Serialize)]
pub struct MicrophoneDeviceInfo {
    pub devices: Vec<String>,
    pub selected: Option<String>,
}

This would eliminate the need for the "DEVICES:" prefix hack and make the API cleaner.

🤖 Prompt for AI Agents
In plugins/listener/src/commands.rs around lines 6 to 29, the function returns
inconsistent response formats by mixing a prefixed JSON string and an optional
string. Refactor the function to always return a serialized struct
MicrophoneDeviceInfo containing a devices vector and an optional selected
string, eliminating the "DEVICES:" prefix and simplifying the API. Define the
struct with serde::Serialize, populate it with the device list and selected
device, serialize it to JSON, and return it consistently as
Ok(Some(json_string)).

Comment on lines 19 to 24
let mut response = std::collections::HashMap::new();
response.insert("devices".to_string(), devices);
response.insert("selected".to_string(), vec![selected_device.unwrap_or_default()]);

let response_json = serde_json::to_string(&response).unwrap_or_default();
return Ok(Some(format!("DEVICES:{}", response_json)));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Improve error handling for JSON serialization.

Using unwrap_or_default() could hide serialization errors.

-        let response_json = serde_json::to_string(&response).unwrap_or_default();
-        return Ok(Some(format!("DEVICES:{}", response_json)));
+        let response_json = serde_json::to_string(&response)
+            .map_err(|e| format!("Failed to serialize device info: {}", e))?;
+        return Ok(Some(format!("DEVICES:{}", response_json)));
🤖 Prompt for AI Agents
In plugins/listener/src/commands.rs around lines 19 to 24, the code uses
unwrap_or_default() on the result of serde_json::to_string, which can silently
ignore serialization errors. Replace unwrap_or_default() with proper error
handling by propagating or logging the serialization error instead of defaulting
to an empty string, ensuring that serialization failures are not hidden.

Comment on lines 591 to 633
StateEvent::MicrophoneDeviceChanged(device_name) => {
// Handle device change during active session with atomic restart
if let Some(session_id) = self.session_id.clone() {
tracing::info!("🎤 ATOMIC microphone device switch to: {:?}", device_name);

// Step 1: Update config atomically
if let Err(e) = self.switch_microphone_device_lightweight(device_name.clone()).await {
tracing::error!("❌ Config update failed: {:?}", e);
return Handled;
}

// Step 2: Atomic restart - no delays, fast cleanup
tracing::info!("⚡ Atomic audio restart");

// Quick cleanup - abort all tasks immediately
if let Some(tx) = self.silence_stream_tx.take() {
let _ = tx.send(());
}
if let Some(mut tasks) = self.tasks.take() {
tasks.abort_all();
// Don't wait for tasks to finish - abort and move on
}

// Reset channels but keep session_id
self.mic_muted_tx = None;
self.mic_muted_rx = None;
self.speaker_muted_tx = None;
self.speaker_muted_rx = None;
self.session_state_tx = None;

// Step 3: Immediate restart with new device (no delays)
match self.setup_resources(&session_id).await {
Ok(_) => {
tracing::info!("✅ ATOMIC microphone switch SUCCESS");
}
Err(e) => {
tracing::error!("❌ ATOMIC switch FAILED: {:?} - going inactive", e);
return Transition(State::inactive());
}
}
}
Handled
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add user notification for device switching failures.

When device switching fails (line 627), the session transitions to inactive but users aren't notified. They might think the app crashed or froze.

         Err(e) => {
-            tracing::error!("❌ ATOMIC switch FAILED: {:?} - going inactive", e);
+            tracing::error!("❌ ATOMIC switch FAILED: {:?} - going inactive", e);
+            // TODO: Emit error event to notify UI
+            let _ = SessionEvent::DeviceSwitchError { 
+                reason: format!("Failed to switch microphone: {}", e) 
+            }.emit(&self.app);
             return Transition(State::inactive());
         }
🤖 Prompt for AI Agents
In plugins/listener/src/fsm.rs around lines 591 to 633, when the device
switching fails at line 627 and the session transitions to inactive, add a user
notification to inform them of the failure. Modify the error handling block to
trigger a user-facing alert or message before returning the Transition to
inactive state, ensuring users are aware of the issue instead of assuming the
app crashed or froze.

Comment on lines +606 to +612
if let Some(tx) = self.silence_stream_tx.take() {
let _ = tx.send(());
}
if let Some(mut tasks) = self.tasks.take() {
tasks.abort_all();
// Don't wait for tasks to finish - abort and move on
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider waiting for critical tasks to prevent resource leaks.

Aborting tasks without joining them could leave resources (file handles, network connections) in an inconsistent state. While speed is important, ensuring clean shutdown prevents issues.

     if let Some(mut tasks) = self.tasks.take() {
         tasks.abort_all();
-        // Don't wait for tasks to finish - abort and move on
+        // Wait briefly for critical tasks to clean up
+        let _ = tokio::time::timeout(
+            Duration::from_millis(100),
+            async {
+                while let Some(_) = tasks.join_next().await {}
+            }
+        ).await;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if let Some(tx) = self.silence_stream_tx.take() {
let _ = tx.send(());
}
if let Some(mut tasks) = self.tasks.take() {
tasks.abort_all();
// Don't wait for tasks to finish - abort and move on
}
if let Some(tx) = self.silence_stream_tx.take() {
let _ = tx.send(());
}
if let Some(mut tasks) = self.tasks.take() {
tasks.abort_all();
// Wait briefly for critical tasks to clean up
let _ = tokio::time::timeout(
Duration::from_millis(100),
async {
while let Some(_) = tasks.join_next().await {}
}
).await;
}
🤖 Prompt for AI Agents
In plugins/listener/src/fsm.rs around lines 606 to 612, the current code aborts
tasks without waiting for their completion, which may cause resource leaks.
Modify the code to await the completion of all aborted tasks after calling
abort_all(), ensuring a clean shutdown by properly releasing resources like file
handles and network connections before proceeding.

Comment on lines 58 to 73
found_device = Some(device);
break;
}
// Try case-insensitive match as fallback
if name.to_lowercase() == device_name.to_lowercase() {
found_device = Some(device);
break;
}
// Try partial match for similar names (e.g., "MacBook Pro Microphone" contains "MacBook")
if name.to_lowercase().contains(&device_name.to_lowercase()) ||
device_name.to_lowercase().contains(&name.to_lowercase()) {
found_device = Some(device);
break;
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Refine device matching logic to prevent unintended matches.

The current partial matching is too permissive. For example, searching for "Book" could match "MacBook Pro Microphone" which might not be intended.

Consider this approach:

  1. Try exact match first (current)
  2. Try case-insensitive exact match (current)
  3. For partial matches, require the search term to be a complete word/token
                         // Try partial match for similar names (e.g., "MacBook Pro Microphone" contains "MacBook")
-                        if name.to_lowercase().contains(&device_name.to_lowercase()) || 
-                           device_name.to_lowercase().contains(&name.to_lowercase()) {
+                        // Only match if the device_name appears as a complete token
+                        let name_lower = name.to_lowercase();
+                        let search_lower = device_name.to_lowercase();
+                        if name_lower.split_whitespace().any(|word| word == search_lower) ||
+                           name_lower.starts_with(&format!("{} ", search_lower)) ||
+                           name_lower.ends_with(&format!(" {}", search_lower)) ||
+                           name_lower.contains(&format!(" {} ", search_lower)) {
                             found_device = Some(device);
                             break;
                         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if name == device_name {
found_device = Some(device);
break;
}
// Try case-insensitive match as fallback
if name.to_lowercase() == device_name.to_lowercase() {
found_device = Some(device);
break;
}
// Try partial match for similar names (e.g., "MacBook Pro Microphone" contains "MacBook")
if name.to_lowercase().contains(&device_name.to_lowercase()) ||
device_name.to_lowercase().contains(&name.to_lowercase()) {
found_device = Some(device);
break;
}
}
if name == device_name {
found_device = Some(device);
break;
}
// Try case-insensitive match as fallback
if name.to_lowercase() == device_name.to_lowercase() {
found_device = Some(device);
break;
}
// Only match if the device_name appears as a complete token
let name_lower = name.to_lowercase();
let search_lower = device_name.to_lowercase();
if name_lower.split_whitespace().any(|word| word == search_lower) ||
name_lower.starts_with(&format!("{} ", search_lower)) ||
name_lower.ends_with(&format!(" {}", search_lower)) ||
name_lower.contains(&format!(" {} ", search_lower)) {
found_device = Some(device);
break;
}
🤖 Prompt for AI Agents
In crates/audio/src/mic.rs between lines 58 and 73, the partial matching logic
for device names is too broad, causing unintended matches like "Book" matching
"MacBook Pro Microphone." To fix this, keep the exact and case-insensitive exact
matches as is, but update the partial match to only succeed if the search term
matches a whole word or token within the device name. Implement this by
splitting the device name and search term into tokens and checking if any token
matches the search term exactly in a case-insensitive manner.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (6)
apps/desktop/src/components/editor-area/note-header/listen-button.tsx (3)

374-422: Extract duplicated device parsing logic into a helper function.

The device data parsing logic is duplicated between microphoneDevices and selectedDevice queries. This violates DRY principle and could lead to maintenance issues.


360-367: Improve error handling to provide user feedback.

The current error handling silently fails by returning null. Users won't know if device enumeration failed due to permissions, API errors, or other issues.


424-442: Add user feedback for device switching errors.

The mutation logs errors to console but doesn't inform users when device switching fails. Users need to know if their action succeeded.

crates/audio/src/mic.rs (3)

78-98: Replace eprintln! with proper logging framework.

Using eprintln! for logging bypasses the application's logging configuration and always outputs to stderr.


67-73: Refine device matching logic to prevent unintended matches.

The current partial matching is too permissive. For example, searching for "Book" could match "MacBook Pro Microphone" which might not be intended.


389-392: Wait for thread cleanup to prevent resource leaks.

The Drop implementation sends a shutdown signal but doesn't wait for the thread to finish.

🧹 Nitpick comments (1)
plugins/listener/src/ext.rs (1)

45-114: Consider reducing logging verbosity for production environments.

While the extensive logging is helpful for debugging device enumeration issues, the current implementation logs at info level for every device found and various fallback scenarios. This could be noisy in production environments with many audio devices.

Consider adjusting log levels:

-                            tracing::info!("Found input device: '{}'", name);
+                            tracing::debug!("Found input device: '{}'", name);

-                tracing::info!(
+                tracing::debug!(
                     "Successfully enumerated {} microphone devices out of {} total devices",
                     device_names.len(),
                     device_count
                 );
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3671916 and 3688fd6.

📒 Files selected for processing (5)
  • apps/desktop/src/components/editor-area/note-header/listen-button.tsx (3 hunks)
  • crates/audio/src/mic.rs (1 hunks)
  • plugins/listener/src/commands.rs (2 hunks)
  • plugins/listener/src/ext.rs (5 hunks)
  • plugins/listener/src/fsm.rs (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • plugins/listener/src/commands.rs
  • plugins/listener/src/fsm.rs
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.{js,ts,tsx,rs}`: 1. No error handling. 2. No unused imports, variables, or functions. 3. For comments, keep it minimal. It should be about "Why", not "What".

**/*.{js,ts,tsx,rs}: 1. No error handling.
2. No unused imports, variables, or functions.
3. For comments, keep it minimal. It should be about "Why", not "What".

⚙️ Source: CodeRabbit Configuration File

List of files the instruction was applied to:

  • plugins/listener/src/ext.rs
  • crates/audio/src/mic.rs
  • apps/desktop/src/components/editor-area/note-header/listen-button.tsx
🧬 Code Graph Analysis (1)
apps/desktop/src/components/editor-area/note-header/listen-button.tsx (4)
packages/ui/src/components/ui/button.tsx (1)
  • Button (37-89)
packages/ui/src/lib/utils.ts (1)
  • cn (4-6)
apps/desktop/src/components/sound-indicator.tsx (1)
  • SoundIndicator (6-31)
packages/ui/src/components/ui/popover.tsx (3)
  • Popover (85-85)
  • PopoverTrigger (85-85)
  • PopoverContent (85-85)
🪛 Biome (1.9.4)
apps/desktop/src/components/editor-area/note-header/listen-button.tsx

[error] 377-377: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 403-403: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: ci (macos, macos-latest)
  • GitHub Check: ci (windows, windows-latest)
  • GitHub Check: ci
🔇 Additional comments (1)
plugins/listener/src/ext.rs (1)

14-21: LGTM! Well-designed trait extensions for microphone device management.

The new async methods provide comprehensive microphone device selection functionality with clear separation of concerns.

@plyght plyght closed this Jul 3, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants