@@ -215,26 +215,29 @@ class VolumeMonitor: ObservableObject {
215215 deviceChangeDebounceTask? . cancel ( )
216216 keyPressDebounceTask? . cancel ( )
217217
218- // CRITICAL: Ensure CoreAudio listeners are removed before deallocation
219- // Must use strong references to dependencies to guarantee cleanup even if deinit runs on background thread
220- let state = self . state
218+ // Capture all state synchronously during deinit before dispatching to main thread.
219+ let capturedDeviceID = state. listeningDeviceIDValue ( )
220+ let capturedVolumeElements = state. registeredVolumeElementsSnapshot ( )
221+ let capturedMuteElements = state. registeredMuteElementsSnapshot ( )
221222 let deviceManager = self . deviceManager
222223 let audioQueue = self . audioQueue
223224 let systemEventMonitor = self . systemEventMonitor
224- let volumeListener = self . volumeListener
225- let muteListener = self . muteListener
226- let deviceListener = self . deviceListener
225+ let capturedVolumeListener = self . volumeListener
226+ let capturedMuteListener = self . muteListener
227+ let capturedDeviceListener = self . deviceListener
227228
228229 // Always dispatch to main thread for @MainActor performCleanup
229230 DispatchQueue . main. async {
230231 Self . performCleanup (
231- state: state,
232+ capturedDeviceID: capturedDeviceID,
233+ capturedVolumeElements: capturedVolumeElements,
234+ capturedMuteElements: capturedMuteElements,
232235 deviceManager: deviceManager,
233236 audioQueue: audioQueue,
234237 systemEventMonitor: systemEventMonitor,
235- volumeListener: volumeListener ,
236- muteListener: muteListener ,
237- deviceListener: deviceListener
238+ volumeListener: capturedVolumeListener ,
239+ muteListener: capturedMuteListener ,
240+ deviceListener: capturedDeviceListener
238241 )
239242 }
240243 }
@@ -632,7 +635,9 @@ class VolumeMonitor: ObservableObject {
632635
633636 @MainActor
634637 private static func performCleanup(
635- state: VolumeStateStore ,
638+ capturedDeviceID: AudioDeviceID ? ,
639+ capturedVolumeElements: [ AudioObjectPropertyElement ] ,
640+ capturedMuteElements: [ AudioObjectPropertyElement ] ,
636641 deviceManager: AudioDeviceManager ,
637642 audioQueue: DispatchQueue ,
638643 systemEventMonitor: SystemEventMonitor ,
@@ -657,12 +662,10 @@ class VolumeMonitor: ObservableObject {
657662 mElement: 0
658663 )
659664
660- let removalDeviceID =
661- state. listeningDeviceIDValue ( ) ?? deviceManager. getDefaultOutputDevice ( )
665+ let removalDeviceID = capturedDeviceID ?? deviceManager. getDefaultOutputDevice ( )
662666
663667 if let volumeListener = volumeListener {
664- let registeredVolumes = state. registeredVolumeElementsSnapshot ( )
665- for element in registeredVolumes {
668+ for element in capturedVolumeElements {
666669 var volumeAddress = deviceManager. makePropertyAddress (
667670 selector: kAudioDevicePropertyVolumeScalar, element: element)
668671 if removalDeviceID != 0 {
@@ -673,8 +676,7 @@ class VolumeMonitor: ObservableObject {
673676 }
674677
675678 if let muteListener = muteListener {
676- let registeredMutes = state. registeredMuteElementsSnapshot ( )
677- for element in registeredMutes {
679+ for element in capturedMuteElements {
678680 var muteAddress = deviceManager. makePropertyAddress (
679681 selector: kAudioDevicePropertyMute, element: element)
680682 if removalDeviceID != 0 {
@@ -688,10 +690,7 @@ class VolumeMonitor: ObservableObject {
688690 AudioObjectRemovePropertyListenerBlock (
689691 AudioObjectID ( kAudioObjectSystemObject) , & deviceAddress, audioQueue, deviceListener)
690692 }
691-
692- state. updateListeningDeviceID ( nil )
693- state. updateRegisteredVolumeElements ( [ ] )
694- state. updateRegisteredMuteElements ( [ ] )
693+ // State and listener properties are already cleared synchronously in stopListening()/deinit.
695694 }
696695
697696 func stopListening( ) {
@@ -703,30 +702,41 @@ class VolumeMonitor: ObservableObject {
703702 // This ensures startListening() called right after will be blocked by the guard check
704703 state. setListeningActive ( false )
705704
706- // Capture dependencies strongly for cleanup
707- let state = self . state
705+ // Synchronously capture and clear all state before dispatching async cleanup.
706+ // Clearing here (not in the async block) ensures that startListening()'s subsequent
707+ // writes are never overwritten by a delayed cleanup dispatch.
708+ let capturedDeviceID = state. listeningDeviceIDValue ( )
709+ let capturedVolumeElements = state. registeredVolumeElementsSnapshot ( )
710+ let capturedMuteElements = state. registeredMuteElementsSnapshot ( )
711+ let capturedVolumeListener = self . volumeListener
712+ let capturedMuteListener = self . muteListener
713+ let capturedDeviceListener = self . deviceListener
714+
715+ state. updateListeningDeviceID ( nil )
716+ state. updateRegisteredVolumeElements ( [ ] )
717+ state. updateRegisteredMuteElements ( [ ] )
718+ self . volumeListener = nil
719+ self . muteListener = nil
720+ self . deviceListener = nil
721+
722+ // Capture strong dependencies for async cleanup
708723 let deviceManager = self . deviceManager
709724 let audioQueue = self . audioQueue
710725 let systemEventMonitor = self . systemEventMonitor
711- let volumeListener = self . volumeListener
712- let muteListener = self . muteListener
713- let deviceListener = self . deviceListener
714726
715727 // Dispatch cleanup to main thread with strong references
716- DispatchQueue . main. async { [ weak self ] in
728+ DispatchQueue . main. async {
717729 Self . performCleanup (
718- state: state,
730+ capturedDeviceID: capturedDeviceID,
731+ capturedVolumeElements: capturedVolumeElements,
732+ capturedMuteElements: capturedMuteElements,
719733 deviceManager: deviceManager,
720734 audioQueue: audioQueue,
721735 systemEventMonitor: systemEventMonitor,
722- volumeListener: volumeListener ,
723- muteListener: muteListener ,
724- deviceListener: deviceListener
736+ volumeListener: capturedVolumeListener ,
737+ muteListener: capturedMuteListener ,
738+ deviceListener: capturedDeviceListener
725739 )
726- // Clear instance listener properties to release closures
727- self ? . volumeListener = nil
728- self ? . muteListener = nil
729- self ? . deviceListener = nil
730740 }
731741 }
732742
0 commit comments