Skip to content

Start MLX capture off the main thread to avoid hotkey freeze#107

Merged
hehehai merged 1 commit into
hehehai:mainfrom
yxs:fix/mlx-async-capture-start
Jun 17, 2026
Merged

Start MLX capture off the main thread to avoid hotkey freeze#107
hehehai merged 1 commit into
hehehai:mainfrom
yxs:fix/mlx-async-capture-start

Conversation

@yxs

@yxs yxs commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Problem

With the local MLX engine, pressing the Fn hotkey to dictate could freeze the app and leave Fn unresponsive, with no overlay feedback.

Root cause: the global hotkey's CGEventTap callback is attached to the main run loop (HotkeyManagerCFRunLoopAddSource(CFRunLoopGetMain(), …)), and MLXTranscriber.startRecording() started AVAudioEngine synchronously on that thread. When CoreAudio is wedged — e.g. coreaudiod stuck after a Continuity/iPhone mic hotplug — AVAudioEngine.start() blocks ~10s and then fails with kAudioHardwareNotRunningError ('stop', on PerformCommand(kAUStartIO)). During those 10s the main thread is blocked, so:

  • the UI freezes and the recording overlay never paints, and
  • the event tap exceeds its timeout and macOS disables it (kCGEventTapDisabledByTimeout), so the Fn hotkey 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 Task and report start failures to the overlay. Only the MLX path was synchronous.

Fix

  • Move the blocking 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.
  • Convert MLX start to the same async startRecordingSession() async -> String? shape the other engines use, surfacing failures via handleRecordingStartFailure (overlay reminder + clean reset) instead of silently resetting.
  • Guard against a stray capture if the user releases Fn while the engine is still starting (mirrors shouldContinueWhisperStartup).

Notes for review

  • The off-main start uses a withThrowingTaskGroup race (same idiom as RemoteProviderConnectivityTester). On timeout it calls engine.stop() to unwind the blocked start(); 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.
  • The underlying wedged-coreaudiod condition is environmental (cleared by killall coreaudiod); this PR only makes the app degrade gracefully instead of freezing.

Testing

  • xcodebuild -scheme Voxt -configuration DebugBUILD SUCCEEDED, no new warnings.
  • The wedged-HAL state could not be reproduced on demand, so the timeout/abort path is verified by code inspection + build only; the happy path is unchanged.

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.
@yxs

yxs commented Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

@hehehai fix with code agent, please review/amend before merge.

@hehehai hehehai added the bug Something isn't working label Jun 17, 2026
@hehehai hehehai merged commit a025a4a into hehehai:main Jun 17, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants