Start MLX capture off the main thread to avoid hotkey freeze#107
Merged
Conversation
With the local MLX engine, pressing the Fn hotkey could freeze the app and leave Fn unresponsive, with no overlay feedback. The global hotkey's CGEventTap callback runs on the main run loop, and MLXTranscriber started AVAudioEngine synchronously on that thread. When CoreAudio is wedged (e.g. coreaudiod stuck after a Continuity mic hotplug), AVAudioEngine.start() blocks ~10s and then fails with kAudioHardwareNotRunningError on PerformCommand(kAUStartIO). During those 10s the main thread is blocked, so the UI freezes and the event tap is disabled by macOS (kCGEventTapDisabledByTimeout). The failure was also only logged, never surfaced, so the overlay just vanished. - Run the blocking AVAudioEngine.start() off the main actor with a 6s timeout so a wedged device can never freeze the hotkey/UI thread. - Convert MLX start to the async startRecordingSession() -> String? shape the other engines use and surface failures via handleRecordingStartFailure (overlay reminder + clean reset) instead of silently resetting. - Stop a stray capture if Fn is released while the engine is still starting.
Contributor
Author
|
@hehehai fix with code agent, please review/amend before merge. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
With the local MLX engine, pressing the
Fnhotkey to dictate could freeze the app and leaveFnunresponsive, with no overlay feedback.Root cause: the global hotkey's
CGEventTapcallback is attached to the main run loop (HotkeyManager→CFRunLoopAddSource(CFRunLoopGetMain(), …)), andMLXTranscriber.startRecording()startedAVAudioEnginesynchronously on that thread. When CoreAudio is wedged — e.g.coreaudiodstuck after a Continuity/iPhone mic hotplug —AVAudioEngine.start()blocks ~10s and then fails withkAudioHardwareNotRunningError('stop', onPerformCommand(kAUStartIO)). During those 10s the main thread is blocked, so:kCGEventTapDisabledByTimeout), so theFnhotkey stops working until it is re-armed.The failure was also only written to the log and never surfaced — unlike the WhisperKit, Speech and Remote paths, which already kick their start onto a
Taskand report start failures to the overlay. Only the MLX path was synchronous.Fix
AVAudioEngine.start()off the main actor and race it against a 6s timeout, so a wedged audio device can no longer freeze the hotkey/UI thread.startRecordingSession() async -> String?shape the other engines use, surfacing failures viahandleRecordingStartFailure(overlay reminder + clean reset) instead of silently resetting.Fnwhile the engine is still starting (mirrorsshouldContinueWhisperStartup).Notes for review
withThrowingTaskGrouprace (same idiom asRemoteProviderConnectivityTester). On timeout it callsengine.stop()to unwind the blockedstart(); that is the one place start/stop can briefly overlap, and it only happens on the wedged-HAL error path. Flag if you'd prefer a different abort mechanism.coreaudiodcondition is environmental (cleared bykillall coreaudiod); this PR only makes the app degrade gracefully instead of freezing.Testing
xcodebuild -scheme Voxt -configuration Debug→ BUILD SUCCEEDED, no new warnings.