Listen for changes to the configuration of the attached device too (#28045)

Conrad Irwin and Zed AI created

Release Notes:

- Fixed an issue causing "robot voice" when enabling the microphone on
some bluetooth headphones (hopefully).

Co-authored-by: Zed AI <ai+claude-3.7@zed.dev>

Change summary

crates/livekit_client/src/livekit_client/playback.rs | 101 +++++++++++++
1 file changed, 94 insertions(+), 7 deletions(-)

Detailed changes

crates/livekit_client/src/livekit_client/playback.rs 🔗

@@ -163,9 +163,8 @@ impl AudioStack {
         sample_rate: u32,
         num_channels: u32,
     ) -> Result<()> {
-        let mut default_change_listener = DeviceChangeListener::new(false)?;
-
         loop {
+            let mut device_change_listener = DeviceChangeListener::new(false)?;
             let (output_device, output_config) = default_device(false)?;
             let (end_on_drop_tx, end_on_drop_rx) = std::sync::mpsc::channel::<()>();
             let mixer = mixer.clone();
@@ -225,7 +224,7 @@ impl AudioStack {
                 end_on_drop_rx.recv().ok();
             });
 
-            default_change_listener.next().await;
+            device_change_listener.next().await;
             drop(end_on_drop_tx)
         }
     }
@@ -236,8 +235,8 @@ impl AudioStack {
         sample_rate: u32,
         num_channels: u32,
     ) -> Result<()> {
-        let mut default_change_listener = DeviceChangeListener::new(true)?;
         loop {
+            let mut device_change_listener = DeviceChangeListener::new(true)?;
             let (device, config) = default_device(true)?;
             let (end_on_drop_tx, end_on_drop_rx) = std::sync::mpsc::channel::<()>();
             let apm = apm.clone();
@@ -310,7 +309,7 @@ impl AudioStack {
                 .log_err();
             });
 
-            default_change_listener.next().await;
+            device_change_listener.next().await;
             drop(end_on_drop_tx)
         }
     }
@@ -646,6 +645,7 @@ mod macos {
         rx: UnboundedReceiver<()>,
         callback: Box<PropertyListenerCallbackWrapper>,
         input: bool,
+        device_id: AudioObjectID, // Store the device ID to properly remove listeners
     }
 
     trait _AssertSend: Send {}
@@ -672,7 +672,9 @@ mod macos {
                 tx.unbounded_send(()).ok();
             })));
 
-            unsafe {
+            // Get the current default device ID
+            let device_id = unsafe {
+                // Listen for default device changes
                 coreaudio::Error::from_os_status(AudioObjectAddPropertyListener(
                     kAudioObjectSystemObject,
                     &AudioObjectPropertyAddress {
@@ -687,12 +689,78 @@ mod macos {
                     Some(property_listener_handler_shim),
                     &*callback as *const _ as *mut _,
                 ))?;
-            }
+
+                // Also listen for changes to the device configuration
+                let device_id = if input {
+                    let mut input_device: AudioObjectID = 0;
+                    let mut prop_size = std::mem::size_of::<AudioObjectID>() as u32;
+                    let result = coreaudio::sys::AudioObjectGetPropertyData(
+                        kAudioObjectSystemObject,
+                        &AudioObjectPropertyAddress {
+                            mSelector: kAudioHardwarePropertyDefaultInputDevice,
+                            mScope: kAudioObjectPropertyScopeGlobal,
+                            mElement: kAudioObjectPropertyElementMaster,
+                        },
+                        0,
+                        std::ptr::null(),
+                        &mut prop_size as *mut _,
+                        &mut input_device as *mut _ as *mut _,
+                    );
+                    if result != 0 {
+                        log::warn!("Failed to get default input device ID");
+                        0
+                    } else {
+                        input_device
+                    }
+                } else {
+                    let mut output_device: AudioObjectID = 0;
+                    let mut prop_size = std::mem::size_of::<AudioObjectID>() as u32;
+                    let result = coreaudio::sys::AudioObjectGetPropertyData(
+                        kAudioObjectSystemObject,
+                        &AudioObjectPropertyAddress {
+                            mSelector: kAudioHardwarePropertyDefaultOutputDevice,
+                            mScope: kAudioObjectPropertyScopeGlobal,
+                            mElement: kAudioObjectPropertyElementMaster,
+                        },
+                        0,
+                        std::ptr::null(),
+                        &mut prop_size as *mut _,
+                        &mut output_device as *mut _ as *mut _,
+                    );
+                    if result != 0 {
+                        log::warn!("Failed to get default output device ID");
+                        0
+                    } else {
+                        output_device
+                    }
+                };
+
+                if device_id != 0 {
+                    // Listen for format changes on the device
+                    coreaudio::Error::from_os_status(AudioObjectAddPropertyListener(
+                        device_id,
+                        &AudioObjectPropertyAddress {
+                            mSelector: coreaudio::sys::kAudioDevicePropertyStreamFormat,
+                            mScope: if input {
+                                coreaudio::sys::kAudioObjectPropertyScopeInput
+                            } else {
+                                coreaudio::sys::kAudioObjectPropertyScopeOutput
+                            },
+                            mElement: kAudioObjectPropertyElementMaster,
+                        },
+                        Some(property_listener_handler_shim),
+                        &*callback as *const _ as *mut _,
+                    ))?;
+                }
+
+                device_id
+            };
 
             Ok(Self {
                 rx,
                 callback,
                 input,
+                device_id,
             })
         }
     }
@@ -700,6 +768,7 @@ mod macos {
     impl Drop for CoreAudioDefaultDeviceChangeListener {
         fn drop(&mut self) {
             unsafe {
+                // Remove the system-level property listener
                 AudioObjectRemovePropertyListener(
                     kAudioObjectSystemObject,
                     &AudioObjectPropertyAddress {
@@ -714,6 +783,24 @@ mod macos {
                     Some(property_listener_handler_shim),
                     &*self.callback as *const _ as *mut _,
                 );
+
+                // Remove the device-specific property listener if we have a valid device ID
+                if self.device_id != 0 {
+                    AudioObjectRemovePropertyListener(
+                        self.device_id,
+                        &AudioObjectPropertyAddress {
+                            mSelector: coreaudio::sys::kAudioDevicePropertyStreamFormat,
+                            mScope: if self.input {
+                                coreaudio::sys::kAudioObjectPropertyScopeInput
+                            } else {
+                                coreaudio::sys::kAudioObjectPropertyScopeOutput
+                            },
+                            mElement: kAudioObjectPropertyElementMaster,
+                        },
+                        Some(property_listener_handler_shim),
+                        &*self.callback as *const _ as *mut _,
+                    );
+                }
             }
         }
     }