Releases: drewburchfield/macos-mic-keepwarm
v0.9.2 - Bluetooth Deadlock Resilience
Improves Bluetooth deadlock resilience with a targeted fix based on spindump evidence from a live deadlock capture.
Root cause (refined)
v0.9.1 dispatched stopRunning() to a background thread to avoid blocking the main thread during Bluetooth teardown. This worked, but the leaked background thread still held a CMIO semaphore that coreaudiod was waiting on, creating a circular deadlock chain:
- coreaudiod IO thread stuck in
HALB_Guard::WaitFor()on dead Bluetooth device (96% of spindump samples) - BTAudioHALPlugin blocked by mutex held by stuck IO thread
- coreaudiod AudioDeviceManager blocked by CMIO semaphore owned by mic-warm
- mic-warm's CMIO thread blocked waiting for coreaudiod to complete teardown
Fixes
- CMIO pipeline teardown:
removeInput()/removeOutput()called on the main thread before dispatchingstopRunning()to background, releasing the semaphore coreaudiod blocks on. Measured teardown at 30-44ms with no deadlock (vs potentially hanging forever) - Stale delegate cleanup: old session's
setSampleBufferDelegate(nil, queue: nil)called during teardown to prevent stale callbacks from corrupting new session's sample count - Thread-safe logging: replaced
DateFormatterwithISO8601DateFormatter(thread-safe by design) to prevent crashes from concurrent logging across main and background threads
Docs
- README updated: describes both delay triggers (hardware sleep after ~60s idle AND Bluetooth device switching regardless of idle state), documents deadlock resilience approach
- Apple Feedback draft updated: includes precise spindump call stack evidence and kTCCServiceAudioCapture finding on macOS 26.2
Full Changelog: v0.9.1...v0.9.2
v0.9.1 - Bluetooth Deadlock Fix
Fixes a deadlock that caused mic-warm to freeze when Bluetooth audio devices (AirPods, etc.) disconnect.
Root cause
When AirPods disconnect while mic-warm is running, the debounced restart calls AVCaptureSession.stopRunning() on the old session. CoreAudio hangs in AudioObjectRemovePropertyListener waiting on a condition variable for the now-dead Bluetooth device. The main thread blocks forever, mic goes cold, and push-to-talk gets the activation delay again.
Fixes
- Bluetooth deadlock fix:
stopRunning()dispatched to a background thread so a hung Bluetooth teardown is a leaked thread, not a dead process - Auto-recovery: heartbeat timer detects stalled streams (>15s no samples) or zero-sample sessions (>30s) and automatically restarts the capture session
- Signal handler safety: removed
stopRunning()from SIGTERM/SIGINT handler to prevent deadlock on shutdown - Data race fix: sample counting moved to main queue to prevent race between audio callback and heartbeat
- KVO crash fix:
removeObserverguard matched toaddObserverto prevent crash on session restart - Watchdog: logs warning if background
stopRunning()hangs for >10s (observability for the Bluetooth deadlock) - DateFormatter thread safety: replaced
DateFormatterwith thread-safeISO8601DateFormatterto prevent crashes from concurrent logging across main and background threads - Stale delegate fix: old session's sample buffer delegate is nil'd before dispatching
stopRunning()to prevent stale callbacks from corrupting new session's sample count
Instrumentation
All diagnostic code (per-device CoreAudio listeners, AVFoundation session lifecycle notifications, verbose heartbeat logging) is retained behind #if false blocks for future debugging. Zero runtime cost.
Full Changelog: v0.9.0...v0.9.1
v0.9.0 - Native Swift Binary
Replaces ffmpeg + SwitchAudioSource with a single native Swift binary. No Homebrew dependencies.
Why this release
Homebrew upgrades move ffmpeg to a new versioned path (e.g. 8.0.1_2 to 8.0.1_3). macOS TCC tracks mic permissions by binary path for unsigned binaries, so every brew upgrade silently breaks the mic permission. The native binary at ~/.local/bin/mic-warm with ad-hoc code signing provides a stable identity that persists across updates.
What's new
- Native Swift binary using AVCaptureSession to hold the mic open (replaces ffmpeg)
- Event-driven device detection via CoreAudio property listener (replaces SwitchAudioSource polling)
- Universal binary (ARM + Intel) built and released automatically via GitHub Actions
- One-line install:
curl -fsSL https://raw.githubusercontent.com/drewburchfield/macos-mic-keepwarm/master/install.sh | bash - Automatic migration from the old ffmpeg-based method
- Signal-safe shutdown on SIGTERM/SIGINT with PID file cleanup
- Recovery loop handles coreaudiod restarts and missing audio devices
What's removed
- No more Homebrew dependency
- No more ffmpeg or SwitchAudioSource requirements
- No more PATH-dependent LaunchAgent configuration
Install
curl -fsSL https://raw.githubusercontent.com/drewburchfield/macos-mic-keepwarm/master/install.sh | bashUninstall
curl -fsSL https://raw.githubusercontent.com/drewburchfield/macos-mic-keepwarm/master/uninstall.sh | bashCompatibility
- macOS 13 Ventura through macOS 26 Tahoe
- Apple Silicon and Intel Macs
v0.2.0 - Device Change Debouncing
What's new
- Debounced device-change detection: 3-second settle window prevents rapid restarts when Bluetooth devices (AirPods, headsets) are connecting. macOS often fires multiple device-change events during a Bluetooth handoff; the debounce waits for things to settle before restarting the audio stream.
- Integration test suite: Automated tests for process lifecycle, signal handling, and device switching.
Requirements
- Homebrew, ffmpeg, SwitchAudioSource (same as v0.1.0)
v0.1.0 - Initial Release
Initial release of macos-mic-keepwarm.
What it does
Keeps the macOS microphone hardware warm to eliminate the 2-5 second push-to-talk activation delay on Apple Silicon Macs.
Implementation
- Shell script (
keep-mic-warm.sh) using ffmpeg to hold the mic input stream open - SwitchAudioSource for device detection
- LaunchAgent for automatic startup
- PID file management for clean process lifecycle
Requirements
- Homebrew
- ffmpeg (
brew install ffmpeg) - SwitchAudioSource (
brew install switchaudio-osx)