@@ -117,8 +117,12 @@ func logAllDevices(prefix: String) {
117117let pidPath = " /tmp/mic-warm.pid "
118118
119119func writePID( ) {
120- try ? " \( ProcessInfo . processInfo. processIdentifier) " . write (
121- toFile: pidPath, atomically: true , encoding: . utf8)
120+ do {
121+ try " \( ProcessInfo . processInfo. processIdentifier) " . write (
122+ toFile: pidPath, atomically: true , encoding: . utf8)
123+ } catch {
124+ log ( " Warning: Could not write PID file \( pidPath) : \( error. localizedDescription) " )
125+ }
122126}
123127
124128func cleanupPID( ) {
@@ -191,7 +195,9 @@ class MicKeeper: NSObject, AVCaptureAudioDataOutputSampleBufferDelegate {
191195 let oldSid = sessionID
192196 let oldSamples = sampleCount
193197 log ( " [session- \( oldSid) ] Stopping old session (samples received: \( oldSamples) ) " )
198+ #if false // Enable for instrumented debugging
194199 old. removeObserver ( self , forKeyPath: " running " )
200+ #endif
195201 // Stop the old session on a background thread. stopRunning() can deadlock
196202 // if the session's Bluetooth device has already disconnected (CoreAudio hangs
197203 // in AudioObjectRemovePropertyListener waiting on a dead device).
@@ -200,6 +206,12 @@ class MicKeeper: NSObject, AVCaptureAudioDataOutputSampleBufferDelegate {
200206 old. stopRunning ( )
201207 log ( " [session- \( oldSid) ] Background: old session stopped " )
202208 }
209+ // Watchdog: warn if stopRunning() hangs (likely deadlocked on dead device)
210+ DispatchQueue . global ( qos: . utility) . asyncAfter ( deadline: . now( ) + 10.0 ) { [ weak old] in
211+ if let old, old. isRunning {
212+ log ( " [session- \( oldSid) ] WARNING: stopRunning() did not complete in 10s (likely deadlocked on dead Bluetooth device) " )
213+ }
214+ }
203215 }
204216 session = nil
205217 stopHeartbeat ( )
@@ -216,15 +228,9 @@ class MicKeeper: NSObject, AVCaptureAudioDataOutputSampleBufferDelegate {
216228 }
217229
218230 // Get CoreAudio device ID for this AVCaptureDevice
219- let allDevices = getAllAudioDeviceIDs ( )
220- let deviceUID = device. uniqueID
221- currentDeviceID = 0
222- for d in allDevices {
223- if getStringProperty ( d, selector: kAudioDevicePropertyDeviceUID) == deviceUID {
224- currentDeviceID = d
225- break
226- }
227- }
231+ currentDeviceID = getAllAudioDeviceIDs ( ) . first {
232+ getStringProperty ( $0, selector: kAudioDevicePropertyDeviceUID) == device. uniqueID
233+ } ?? 0
228234
229235 log ( " [session- \( sid) ] Opening device: \( device. localizedName) [id= \( currentDeviceID) ] " )
230236
@@ -242,8 +248,7 @@ class MicKeeper: NSObject, AVCaptureAudioDataOutputSampleBufferDelegate {
242248 }
243249
244250 let output = AVCaptureAudioDataOutput ( )
245- let queue = DispatchQueue ( label: " mic-warm.audio " , qos: . userInitiated)
246- output. setSampleBufferDelegate ( self , queue: queue)
251+ output. setSampleBufferDelegate ( self , queue: . main)
247252 s. addOutput ( output)
248253
249254 log ( " [session- \( sid) ] Starting session... " )
@@ -301,7 +306,7 @@ class MicKeeper: NSObject, AVCaptureAudioDataOutputSampleBufferDelegate {
301306 let timer = DispatchSource . makeTimerSource ( queue: . main)
302307 timer. schedule ( deadline: . now( ) + 5.0 , repeating: 5.0 )
303308 timer. setEventHandler { [ weak self] in
304- guard let self else { return }
309+ guard let self, self . sessionID == sid else { return }
305310 let delta = self . sampleCount - self . lastHeartbeatSampleCount
306311 let running = self . session? . isRunning ?? false
307312 let gap = self . lastSampleTime. map { Date ( ) . timeIntervalSince ( $0) } ?? - 1
@@ -377,6 +382,7 @@ class MicKeeper: NSObject, AVCaptureAudioDataOutputSampleBufferDelegate {
377382 self . debouncedRestart ( reason: " default input device changed (new: \( currentDevice? . localizedName ?? " nil " ) ) " )
378383 }
379384
385+ #if false // Enable for instrumented debugging
380386 // Device list changes (additions/removals)
381387 var devicesAddr = AudioObjectPropertyAddress (
382388 mSelector: kAudioHardwarePropertyDevices,
@@ -385,10 +391,8 @@ class MicKeeper: NSObject, AVCaptureAudioDataOutputSampleBufferDelegate {
385391 AudioObjectAddPropertyListenerBlock (
386392 AudioObjectID ( kAudioObjectSystemObject) , & devicesAddr, DispatchQueue . main
387393 ) { _, _ in
388- #if false // Enable for instrumented debugging
389394 log ( " [system] *** Device list changed " )
390395 logAllDevices ( prefix: " [system] " )
391- #endif
392396 }
393397
394398 // Default output device changes (relevant for Bluetooth mode switching)
@@ -399,15 +403,14 @@ class MicKeeper: NSObject, AVCaptureAudioDataOutputSampleBufferDelegate {
399403 AudioObjectAddPropertyListenerBlock (
400404 AudioObjectID ( kAudioObjectSystemObject) , & outputAddr, DispatchQueue . main
401405 ) { _, _ in
402- #if false // Enable for instrumented debugging
403406 let outputID : AudioDeviceID ? = getAudioProperty (
404407 AudioObjectID ( kAudioObjectSystemObject) ,
405408 selector: kAudioHardwarePropertyDefaultOutputDevice)
406409 if let oid = outputID {
407410 log ( " [system] Default output device changed -> \( describeDevice ( oid) ) " )
408411 }
409- #endif
410412 }
413+ #endif
411414 }
412415
413416 #if false // Enable for instrumented debugging
@@ -591,7 +594,8 @@ class MicKeeper: NSObject, AVCaptureAudioDataOutputSampleBufferDelegate {
591594 }
592595
593596 func signalShutdown( ) {
594- session? . stopRunning ( )
597+ // Skip stopRunning() here: it can deadlock on Bluetooth devices, and
598+ // _exit(0) follows immediately so the OS reclaims all resources.
595599 session = nil
596600 cleanupPID ( )
597601 }
0 commit comments