-
Notifications
You must be signed in to change notification settings - Fork 412
fix(audio): prevent device switching loop during initialization on macOS #1654
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Fixes fastrepl#1371 On macOS 15.x, creating a private aggregate device for speaker audio capture triggers system-wide device change notifications. The DeviceMonitor was reacting to these events during initialization, causing the audio pipeline to restart in a loop and never stabilize. This change adds an initialization_complete flag that prevents device change events from being processed until both mic and speaker streams have successfully initialized. The flag is reset when the user explicitly changes devices to allow proper reinitialization. Root Cause: - PR fastrepl#1471 introduced aggregate device for speaker capture - Creating "private" aggregate device unexpectedly triggers device change events - DeviceMonitor caught in reinitialization loop during startup - Audio pipeline never stabilized, no audio captured Solution: - Added Arc<AtomicBool> to track initialization completion - Device events ignored during initialization phase - Flag set to true after both mic+speaker streams start - Flag reset on explicit device changes for proper reinitialization Testing: - Verified on macOS 15.7.2 (M4 MacBook Pro) - Audio capture now works correctly - No device switching loop observed - Device changes handled gracefully during recording - Success message "audio_streams_initialized" confirms stable pipeline Changes: - Modified: plugins/listener/src/actors/source.rs (+17 lines) - No breaking changes - Backward compatible
📝 WalkthroughPre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (4)
plugins/listener/src/actors/source.rs (4)
69-70: Core fix looks good, but consider stronger memory ordering.The initialization guard successfully prevents device events from triggering restarts during initialization. The logic is sound and the logging will aid debugging.
However, using
Ordering::Relaxedat line 83 may allow subtle race conditions where the device event thread doesn't immediately observe thetruevalue written by the tokio task. While unlikely to cause issues in practice (worst case: one extra event gets ignored after init completes), consider usingOrdering::Acquirefor the load andOrdering::Releasefor the stores at lines 264 and 329 to guarantee visibility across threads.Apply this diff for stronger ordering guarantees:
- if !initialization_complete_clone.load(Ordering::Relaxed) { + if !initialization_complete_clone.load(Ordering::Acquire) {And update the stores at lines 264, 329, and 161:
- initialization_complete.store(true, Ordering::Relaxed); + initialization_complete.store(true, Ordering::Release);- st.initialization_complete.store(false, Ordering::Relaxed); + st.initialization_complete.store(false, Ordering::Release);Also applies to: 83-86, 132-132
264-265: Initialization signaling correctly placed.Setting
initialization_completetotrueafter both streams are created and pinned is the right timing—streams are ready to process audio, and device events should now be handled normally.Optional improvement: If stream creation fails (e.g., at lines 250 or 256), the
unwrap()calls will panic and the flag will never be set totrue, causing all subsequent device events to be ignored until manual intervention. Consider propagating errors gracefully to ensure the system can recover or at least log a fatal error.
329-330: Initialization signaling correctly placed.This mirrors the Single mode path—setting
initialization_completetotrueafter streams are ready is correct.Optional improvement: Same as the Single mode path (lines 264-265), consider propagating stream creation errors gracefully rather than panicking on
unwrap()at lines 318 or 323 to prevent the system from entering an unrecoverable state where device events are permanently ignored.
312-314: Clarify unreachable non-macOS Single mode path.The code at lines 312-314 spawns an empty task for non-macOS in Single mode. However, line 219 ensures non-macOS systems always use
ChannelMode::Dual, making this branch unreachable in practice. If this branch somehow executed,initialization_completewould never be set totrue, permanently blocking device event handling.Consider adding a comment or assertion to make it explicit that this branch is defensive and should never execute, or handle it more explicitly.
#[cfg(not(target_os = "macos"))] { + // Defensive: non-macOS should always use Dual mode (line 219) + tracing::error!("non_macos_single_mode_unexpected"); tokio::spawn(async move {}) }
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
plugins/listener/src/actors/source.rs(8 hunks)
🔇 Additional comments (3)
plugins/listener/src/actors/source.rs (3)
1-2: LGTM: Appropriate types for the initialization guard.The
Arc<AtomicBool>is the correct choice for sharing the initialization state across the device event thread and the tokio task.Also applies to: 43-43
161-161: Correct handling of manual device changes.Resetting
initialization_completetofalsewhen the device is manually changed ensures that subsequent stream initialization is also protected from device event loops. The flag will be set back totruewhenstart_source_loopcompletes initialization.
206-206: LGTM: Proper Arc cloning for task.The clone correctly passes a reference to the tokio task so it can signal completion after stream initialization.
Fix: Audio device switching loop on macOS 15.7.2
Fixes #1371
Problem
On macOS 15.7.2 (M4 MacBook Pro), Hyprnote fails to capture audio due to an infinite device switching loop between the speaker (Device 84) and microphone (Device 91). This causes:
Root Cause
When initializing audio streams, creating the speaker tap's aggregate device triggers system device change notifications. The device monitor responds to these notifications by restarting the streams, which triggers more notifications, creating an infinite loop.
The existing 1000ms debouncing is insufficient during initialization on newer macOS versions.
Solution
Add an initialization guard flag (
initialization_complete) that prevents the device monitor from responding to device change events during audio stream initialization.Key Changes
Arc<AtomicBool>flag toSourceStateinitialization_complete == falsetrueafter both mic and speaker streams are fully initializedfalsewhen manually changing devicesTesting
Tested on macOS 15.7.2 M4 MacBook Pro:
Files Changed
plugins/listener/src/actors/source.rs- Single file modificationLog Output (After Fix)
Instead of the previous loop pattern showing repeated device switching.