Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions webrtc-sys/include/livekit/adm_proxy.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
#include "modules/audio_device/include/audio_device_defines.h"
#include "rtc_base/synchronization/mutex.h"

#if defined(WEBRTC_MAC) && !defined(WEBRTC_IOS)
#include <CoreAudio/CoreAudio.h>
#endif

namespace webrtc {
class Thread;
} // namespace webrtc
Expand Down Expand Up @@ -272,6 +276,45 @@ class AdmProxy : public webrtc::AudioDeviceModule {
uint16_t selected_recording_device_ = 0;
std::string selected_playout_guid_;
std::string selected_recording_guid_;

#if defined(WEBRTC_MAC) && !defined(WEBRTC_IOS)
// macOS only: the precompiled CoreAudio ADM does not re-read the output device's
// nominal sample rate when it changes underneath an active playout (e.g. a Bluetooth
// headset flipping HFP<->A2DP when the mic stream opens/closes). That leaves playout
// emitting samples at the old rate into a device now running at a new rate, which
// pitch-shifts remote audio ("squeaky"). We install a Core Audio property listener on
// the default output device's nominal sample rate (and on default-output-device
// changes) and re-init platform playout when the rate changes so the ADM rebuilds its
// resampler at the correct rate.

// Installs the output sample-rate / default-output-device listeners. Idempotent.
// Must be called with mutex_ held.
void InstallOutputRateListenerLocked();

// Removes the listeners installed by InstallOutputRateListenerLocked(). Idempotent.
// Must be called with mutex_ held.
void RemoveOutputRateListenerLocked();

// Re-initializes platform playout (StopPlayout -> InitPlayout -> StartPlayout) so the
// precompiled ADM re-reads the current output device rate. Runs on worker_thread_.
void RebouncePlayoutForRateChange();

// Core Audio property-listener trampoline. Fires on a Core Audio managed thread for
// default-output-device and nominal-sample-rate changes; posts the rebounce to
// worker_thread_. inClientData is the owning AdmProxy*.
static OSStatus OnOutputPropertyChanged(
AudioObjectID inObjectID,
UInt32 inNumberAddresses,
const AudioObjectPropertyAddress* inAddresses,
void* inClientData);

bool output_listener_installed_ = false;
// Device the nominal-rate listener is currently attached to (kAudioObjectUnknown when
// none). Tracked so we can re-point the listener when the default output device changes.
AudioObjectID listened_output_device_ = kAudioObjectUnknown;
// Guards against re-entrant rebounces triggered by our own StopPlayout/StartPlayout.
bool rebouncing_ = false;
#endif // defined(WEBRTC_MAC) && !defined(WEBRTC_IOS)
};

} // namespace livekit_ffi
191 changes: 190 additions & 1 deletion webrtc-sys/src/adm_proxy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,16 @@ AdmProxy::AdmProxy(const webrtc::Environment& env, webrtc::Thread* worker_thread
AdmProxy::~AdmProxy() {
RTC_LOG(LS_VERBOSE) << "AdmProxy::~AdmProxy()";

#if defined(WEBRTC_MAC) && !defined(WEBRTC_IOS)
// Ensure no Core Audio callback can fire into a destroyed instance. Destruction runs
// on worker_thread_ (see PeerConnectionFactory), the same thread RebouncePlayout tasks
// run on, so a removal here serializes ahead of any later callback-posted task.
{
webrtc::MutexLock lock(&mutex_);
RemoveOutputRateListenerLocked();
}
#endif

if (synthetic_adm_) {
synthetic_adm_->Terminate();
synthetic_adm_ = nullptr;
Expand Down Expand Up @@ -246,8 +256,14 @@ void AdmProxy::SwitchPlayoutModeIfNeeded() {
platform_adm_->InitPlayout();
platform_adm_->StartPlayout();
}
#if defined(WEBRTC_MAC) && !defined(WEBRTC_IOS)
InstallOutputRateListenerLocked();
#endif
} else {
// Switch to synthetic mode - stop platform ADM, start synthetic ADM
#if defined(WEBRTC_MAC) && !defined(WEBRTC_IOS)
RemoveOutputRateListenerLocked();
#endif
if (platform_adm_) {
platform_adm_->StopPlayout();
}
Expand All @@ -273,6 +289,167 @@ void AdmProxy::SwitchRecordingAdmIfNeeded() {
}
}

// =============================================================================
// macOS: Output Sample-Rate Change Handling
// =============================================================================
//
// The precompiled CoreAudio ADM does not re-read the output device's nominal sample
// rate when it changes underneath active playout. A Bluetooth headset flips HFP<->A2DP
// when the microphone stream opens/closes (e.g. on mute, which stops recording),
// changing the output rate. The ADM keeps emitting samples at the old rate into a
// device now running at the new rate, pitch-shifting remote audio. We listen for the
// rate change and re-init platform playout so the ADM rebuilds its resampler.

#if defined(WEBRTC_MAC) && !defined(WEBRTC_IOS)

namespace {

constexpr AudioObjectPropertyAddress kDefaultOutputDeviceAddress = {
kAudioHardwarePropertyDefaultOutputDevice,
kAudioObjectPropertyScopeGlobal,
kAudioObjectPropertyElementMain,
};

constexpr AudioObjectPropertyAddress kNominalSampleRateAddress = {
kAudioDevicePropertyNominalSampleRate,
kAudioObjectPropertyScopeGlobal,
kAudioObjectPropertyElementMain,
};

AudioObjectID CurrentDefaultOutputDevice() {
AudioObjectID device = kAudioObjectUnknown;
UInt32 size = sizeof(device);
OSStatus status =
AudioObjectGetPropertyData(kAudioObjectSystemObject, &kDefaultOutputDeviceAddress,
0, nullptr, &size, &device);
if (status != noErr) {
RTC_LOG(LS_WARNING) << "AdmProxy: failed to read default output device, status="
<< status;
return kAudioObjectUnknown;
}
return device;
}

double OutputNominalSampleRate(AudioObjectID device) {
if (device == kAudioObjectUnknown) return 0.0;
Float64 rate = 0.0;
UInt32 size = sizeof(rate);
OSStatus status = AudioObjectGetPropertyData(device, &kNominalSampleRateAddress, 0,
nullptr, &size, &rate);
if (status != noErr) return 0.0;
return static_cast<double>(rate);
}

} // namespace

OSStatus AdmProxy::OnOutputPropertyChanged(
AudioObjectID /*inObjectID*/,
UInt32 /*inNumberAddresses*/,
const AudioObjectPropertyAddress* /*inAddresses*/,
void* inClientData) {
auto* self = static_cast<AdmProxy*>(inClientData);
if (!self) return noErr;
// Core Audio invokes this on its own thread. Never touch the ADM or mutex_ here;
// hop to worker_thread_, which owns ADM playout lifecycle.
webrtc::Thread* worker = self->worker_thread_;
if (worker) {
worker->PostTask([self] { self->RebouncePlayoutForRateChange(); });
}
return noErr;
}

void AdmProxy::InstallOutputRateListenerLocked() {
if (output_listener_installed_) return;

// Default-output-device changes (headphones plug/unplug, Bluetooth connect) so we can
// re-point the per-device rate listener.
OSStatus status = AudioObjectAddPropertyListener(
kAudioObjectSystemObject, &kDefaultOutputDeviceAddress,
&AdmProxy::OnOutputPropertyChanged, this);
if (status != noErr) {
RTC_LOG(LS_WARNING)
<< "AdmProxy: failed to add default-output-device listener, status=" << status;
}

// In-place nominal sample-rate changes on the current output device (the BT HFP<->A2DP
// flip is the motivating case).
listened_output_device_ = CurrentDefaultOutputDevice();
if (listened_output_device_ != kAudioObjectUnknown) {
status = AudioObjectAddPropertyListener(listened_output_device_,
&kNominalSampleRateAddress,
&AdmProxy::OnOutputPropertyChanged, this);
if (status != noErr) {
RTC_LOG(LS_WARNING)
<< "AdmProxy: failed to add nominal-sample-rate listener, status=" << status;
}
}

output_listener_installed_ = true;
RTC_LOG(LS_INFO) << "AdmProxy: installed output rate listeners (device="
<< listened_output_device_
<< ", rate=" << OutputNominalSampleRate(listened_output_device_)
<< ")";
}

void AdmProxy::RemoveOutputRateListenerLocked() {
if (!output_listener_installed_) return;

AudioObjectRemovePropertyListener(kAudioObjectSystemObject, &kDefaultOutputDeviceAddress,
&AdmProxy::OnOutputPropertyChanged, this);

if (listened_output_device_ != kAudioObjectUnknown) {
AudioObjectRemovePropertyListener(listened_output_device_, &kNominalSampleRateAddress,
&AdmProxy::OnOutputPropertyChanged, this);
listened_output_device_ = kAudioObjectUnknown;
}

output_listener_installed_ = false;
RTC_LOG(LS_INFO) << "AdmProxy: removed output rate listeners";
}

void AdmProxy::RebouncePlayoutForRateChange() {
webrtc::MutexLock lock(&mutex_);

// Ignore irrelevant or re-entrant notifications: our own StopPlayout/StartPlayout below
// can themselves emit property-change callbacks.
if (rebouncing_ || !playing_ || !is_platform_playout_active() || !platform_adm_) {
return;
}
rebouncing_ = true;

// If the default output device changed, move the nominal-rate listener to the new one.
if (output_listener_installed_) {
AudioObjectID current = CurrentDefaultOutputDevice();
if (current != listened_output_device_) {
if (listened_output_device_ != kAudioObjectUnknown) {
AudioObjectRemovePropertyListener(listened_output_device_,
&kNominalSampleRateAddress,
&AdmProxy::OnOutputPropertyChanged, this);
}
listened_output_device_ = current;
if (current != kAudioObjectUnknown) {
AudioObjectAddPropertyListener(current, &kNominalSampleRateAddress,
&AdmProxy::OnOutputPropertyChanged, this);
}
}
}

double rate = OutputNominalSampleRate(listened_output_device_);

// Re-init platform playout so the precompiled CoreAudio ADM re-queries the device's
// current nominal rate and rebuilds its playout resampler.
platform_adm_->StopPlayout();
platform_adm_->InitPlayout();
platform_adm_->StartPlayout();

RTC_LOG(LS_INFO) << "AdmProxy: rebounced platform playout after output rate change "
<< "(device=" << listened_output_device_ << ", rate=" << rate << ")";

rebouncing_ = false;
}

#endif // defined(WEBRTC_MAC) && !defined(WEBRTC_IOS)

// =============================================================================
// AudioDeviceModule Interface Implementation
// =============================================================================
Expand Down Expand Up @@ -308,6 +485,10 @@ int32_t AdmProxy::Init() {
int32_t AdmProxy::Terminate() {
webrtc::MutexLock lock(&mutex_);

#if defined(WEBRTC_MAC) && !defined(WEBRTC_IOS)
RemoveOutputRateListenerLocked();
#endif

int32_t result = 0;
if (synthetic_adm_) {
result = synthetic_adm_->Terminate();
Expand Down Expand Up @@ -503,7 +684,11 @@ int32_t AdmProxy::StartPlayout() {

if (is_platform_playout_active()) {
if (platform_adm_) {
return platform_adm_->StartPlayout();
int32_t result = platform_adm_->StartPlayout();
#if defined(WEBRTC_MAC) && !defined(WEBRTC_IOS)
if (result == 0) InstallOutputRateListenerLocked();
#endif
return result;
}
return -1;
}
Expand All @@ -519,6 +704,10 @@ int32_t AdmProxy::StopPlayout() {
webrtc::MutexLock lock(&mutex_);
playing_ = false;

#if defined(WEBRTC_MAC) && !defined(WEBRTC_IOS)
RemoveOutputRateListenerLocked();
#endif

// Stop both ADMs
if (synthetic_adm_) {
synthetic_adm_->StopPlayout();
Expand Down
Loading