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
5 changes: 5 additions & 0 deletions modules/audio_device/audio_engine_device.h
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,11 @@ class AudioEngineDevice : public AudioDeviceModule, public AudioSessionObserver
std::vector<AudioObjectID> output_device_ids_;
std::vector<std::string> output_device_labels_;
std::vector<std::string> input_device_labels_;

// Private aggregate device used when voice processing is disabled and the
// engine's shared I/O unit must address different input and output devices.
AudioObjectID engine_aggregate_device_id_ = kAudioObjectUnknown;
void DestroyAggregateDeviceIfNeeded();
#endif

bool IsMicrophonePermissionGranted();
Expand Down
129 changes: 127 additions & 2 deletions modules/audio_device/audio_engine_device.mm
Original file line number Diff line number Diff line change
Expand Up @@ -2233,6 +2233,9 @@ AVAudioVoiceProcessingOtherAudioDuckingLevel ToAVDuckingLevel(
}

engine_device_ = nil;
#if TARGET_OS_OSX
DestroyAggregateDeviceIfNeeded();
#endif
}

// --------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -2368,6 +2371,111 @@ AVAudioVoiceProcessingOtherAudioDuckingLevel ToAVDuckingLevel(
}
}

// --------------------------------------------------------------------------------------------
// Step: Configure device for the shared I/O unit (macOS, voice processing disabled)
//
// Without voice processing the engine's input and output nodes share a single
// HAL I/O unit, so per-direction device selection is impossible: setting
// kAudioOutputUnitProperty_CurrentDevice re-routes both directions. When the
// requested input and output devices differ, a private aggregate device
// combining them is created and set instead. This must happen before the
// enable steps below: the HAL unit rejects device changes once the graph is
// wired, and the node formats read during enable must reflect the device.
#if TARGET_OS_OSX
if (!state.next.voice_processing_enabled && state.next.IsAnyEnabled() &&
(!state.prev.IsAnyEnabled() || state.IsEngineRecreateRequired())) {
bool input_needed = state.next.IsInputEnabled();
bool output_needed = state.next.IsOutputEnabled();
AudioObjectID requested_input = state.next.input_device_id;
AudioObjectID requested_output = state.next.output_device_id;

// When both directions follow the system default there is nothing to do,
// the engine already tracks the default route.
if ((input_needed && requested_input != kAudioObjectUnknown) ||
(output_needed && requested_output != kAudioObjectUnknown)) {
AudioObjectID input_device = requested_input;
if (input_needed && input_device == kAudioObjectUnknown) {
input_device =
mac_audio_utils::GetDefaultInputDeviceID().value_or(kAudioObjectUnknown);
}
AudioObjectID output_device = requested_output;
if (output_needed && output_device == kAudioObjectUnknown) {
output_device =
mac_audio_utils::GetDefaultOutputDeviceID().value_or(kAudioObjectUnknown);
}

AudioObjectID target_device = kAudioObjectUnknown;
if (input_needed && output_needed && input_device != kAudioObjectUnknown &&
output_device != kAudioObjectUnknown && input_device != output_device) {
DestroyAggregateDeviceIfNeeded();
auto aggregate =
mac_audio_utils::CreatePrivateAggregateDevice(output_device, input_device);
if (!aggregate.has_value()) {
LOGE() << "Failed to create aggregate device, output=" << output_device
<< " input=" << input_device;
return rollback(kAudioEngineRecordingDeviceNotAvailableError);
}
engine_aggregate_device_id_ = *aggregate;
target_device = *aggregate;
LOGI() << "Created aggregate device " << target_device
<< " (output=" << output_device << ", input=" << input_device << ")";
rollback_actions.push_back([this]() {
RTC_DCHECK_RUN_ON(thread_);
DestroyAggregateDeviceIfNeeded();
});
} else if (input_needed && input_device != kAudioObjectUnknown &&
(!output_needed || input_device == output_device)) {
target_device = input_device;
} else if (output_needed && output_device != kAudioObjectUnknown &&
!input_needed) {
target_device = output_device;
}

if (target_device != kAudioObjectUnknown) {
auto device_name = mac_audio_utils::GetDeviceName(target_device);
LOGI() << "Setting shared I/O unit device: "
<< device_name.value_or("Unknown") << " (" << target_device << ")";
AudioUnit io_unit =
output_needed ? outputNode().audioUnit : inputNode().audioUnit;
OSStatus err = AudioUnitSetProperty(
io_unit, kAudioOutputUnitProperty_CurrentDevice, kAudioUnitScope_Global,
0, &target_device, sizeof(target_device));
if (err != noErr) {
LOGE() << "Failed to set shared I/O unit device: requested="
<< target_device << ", error: " << err;
return rollback(input_needed ? kAudioEngineRecordingDeviceNotAvailableError
: kAudioEnginePlayoutDeviceNotAvailableError);
}

// The unit renegotiates its formats from the new device asynchronously,
// aggregates in particular can take a moment. Wait until the node
// formats are usable so the enable steps below read valid channel
// counts.
constexpr int kMaxFormatAttempts = 100;
constexpr int64_t kFormatPollIntervalMs = 10;
bool format_ready = false;
for (int attempt = 0; attempt < kMaxFormatAttempts; ++attempt) {
bool output_ready =
!output_needed || [outputNode() outputFormatForBus:0].channelCount > 0;
bool input_ready =
!input_needed || [inputNode() outputFormatForBus:0].channelCount > 0;
if (output_ready && input_ready) {
format_ready = true;
break;
}
webrtc::Thread::SleepMs(kFormatPollIntervalMs);
}
if (!format_ready) {
LOGE() << "Shared I/O unit formats did not become ready for device "
<< target_device;
return rollback(input_needed ? kAudioEngineRecordingDeviceNotAvailableError
: kAudioEnginePlayoutDeviceNotAvailableError);
}
}
}
}
#endif

// --------------------------------------------------------------------------------------------
// Step: Enable output
//
Expand Down Expand Up @@ -2824,10 +2932,14 @@ AVAudioVoiceProcessingOtherAudioDuckingLevel ToAVDuckingLevel(
}

// --------------------------------------------------------------------------------------------
// Step: Configure device (macOS only)
// Step: Configure device (macOS, voice processing enabled)
//
// With voice processing the input and output nodes use separate I/O units
// that accept per-direction device selection at this point. The non voice
// processing path configures its shared unit earlier, before the graph is
// wired (see "Configure device for the shared I/O unit" above).
#if TARGET_OS_OSX
if (state.next.IsAnyEnabled() &&
if (state.next.voice_processing_enabled && state.next.IsAnyEnabled() &&
(!state.prev.IsAnyEnabled() || state.IsEngineRecreateRequired())) {
if (state.next.IsInputEnabled()) {
uint32_t requested_input_device_id = state.next.input_device_id;
Expand Down Expand Up @@ -3060,6 +3172,9 @@ AVAudioVoiceProcessingOtherAudioDuckingLevel ToAVDuckingLevel(

LOGI() << "Releasing AVAudioEngine...";
engine_device_ = nil;
#if TARGET_OS_OSX
DestroyAggregateDeviceIfNeeded();
#endif
}

// --- Diagnostic: final state after apply ---
Expand Down Expand Up @@ -3145,6 +3260,16 @@ AVAudioVoiceProcessingOtherAudioDuckingLevel ToAVDuckingLevel(

#if TARGET_OS_OSX

void AudioEngineDevice::DestroyAggregateDeviceIfNeeded() {
RTC_DCHECK_RUN_ON(thread_);
if (engine_aggregate_device_id_ == kAudioObjectUnknown) {
return;
}
LOGI() << "Destroying aggregate device " << engine_aggregate_device_id_;
mac_audio_utils::DestroyAggregateDevice(engine_aggregate_device_id_);
engine_aggregate_device_id_ = kAudioObjectUnknown;
}

void AudioEngineDevice::UpdateAllDeviceIDs() {
using namespace webrtc::mac_audio_utils;

Expand Down
114 changes: 114 additions & 0 deletions modules/audio_device/mac/audio_device_utils_mac.cc
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@
#include "audio_device_utils_mac.h"

#include <IOKit/audio/IOAudioTypes.h>
#include <unistd.h>

#include <atomic>
#include <string>
#include <unordered_set>
#include <utility>
#include <vector>
Expand Down Expand Up @@ -381,5 +384,116 @@ bool IsOutputDevice(AudioObjectID device_id) {
(num_unknown_output_streams > 0 && num_input_streams == 0);
}

std::optional<AudioObjectID> CreatePrivateAggregateDevice(
AudioObjectID output_device_id, AudioObjectID input_device_id) {
std::optional<std::string> output_uid = GetDeviceUniqueID(output_device_id);
std::optional<std::string> input_uid = GetDeviceUniqueID(input_device_id);
if (!output_uid.has_value() || !input_uid.has_value()) {
RTC_LOG(LS_ERROR) << "CreatePrivateAggregateDevice: missing device UID"
<< " (output=" << output_device_id
<< ", input=" << input_device_id << ")";
return std::nullopt;
}

// The aggregate UID must be unique. Multiple instances may exist briefly
// during engine recreation, so include a counter.
static std::atomic<uint64_t> counter{0};
const std::string aggregate_uid = "org.webrtc.audioengine.aggregate." +
std::to_string(getpid()) + "." +
std::to_string(counter.fetch_add(1));

CFStringRef aggregate_uid_cf = CFStringCreateWithCString(
kCFAllocatorDefault, aggregate_uid.c_str(), kNarrowStringEncoding);
CFStringRef output_uid_cf = CFStringCreateWithCString(
kCFAllocatorDefault, output_uid->c_str(), kNarrowStringEncoding);
CFStringRef input_uid_cf = CFStringCreateWithCString(
kCFAllocatorDefault, input_uid->c_str(), kNarrowStringEncoding);

CFMutableDictionaryRef output_sub_device = CFDictionaryCreateMutable(
kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks);
CFDictionarySetValue(output_sub_device, CFSTR(kAudioSubDeviceUIDKey),
output_uid_cf);

// The output device drives the clock, so the input sub device needs drift
// compensation.
int32_t drift_compensation = 1;
CFNumberRef drift_compensation_cf = CFNumberCreate(
kCFAllocatorDefault, kCFNumberSInt32Type, &drift_compensation);
CFMutableDictionaryRef input_sub_device = CFDictionaryCreateMutable(
kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks);
CFDictionarySetValue(input_sub_device, CFSTR(kAudioSubDeviceUIDKey),
input_uid_cf);
CFDictionarySetValue(input_sub_device,
CFSTR(kAudioSubDeviceDriftCompensationKey),
drift_compensation_cf);

const void* sub_devices[] = {output_sub_device, input_sub_device};
CFArrayRef sub_device_list =
CFArrayCreate(kCFAllocatorDefault, sub_devices, 2, &kCFTypeArrayCallBacks);

int32_t is_private = 1;
CFNumberRef is_private_cf =
CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &is_private);

CFMutableDictionaryRef description = CFDictionaryCreateMutable(
kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks);
CFDictionarySetValue(description, CFSTR(kAudioAggregateDeviceUIDKey),
aggregate_uid_cf);
CFDictionarySetValue(description, CFSTR(kAudioAggregateDeviceNameKey),
CFSTR("WebRTC AudioEngine I/O"));
CFDictionarySetValue(description, CFSTR(kAudioAggregateDeviceSubDeviceListKey),
sub_device_list);
CFDictionarySetValue(description,
CFSTR(kAudioAggregateDeviceMainSubDeviceKey),
output_uid_cf);
CFDictionarySetValue(description, CFSTR(kAudioAggregateDeviceIsPrivateKey),
is_private_cf);

AudioObjectID aggregate_device_id = kAudioObjectUnknown;
OSStatus status =
AudioHardwareCreateAggregateDevice(description, &aggregate_device_id);

CFRelease(description);
CFRelease(is_private_cf);
CFRelease(sub_device_list);
CFRelease(input_sub_device);
CFRelease(drift_compensation_cf);
CFRelease(output_sub_device);
CFRelease(input_uid_cf);
CFRelease(output_uid_cf);
CFRelease(aggregate_uid_cf);

if (status != noErr || aggregate_device_id == kAudioObjectUnknown) {
RTC_LOG(LS_ERROR) << "AudioHardwareCreateAggregateDevice failed: "
<< status;
return std::nullopt;
}

// The HAL may still be composing the sub devices at this point. Consumers
// must not rely on the aggregate's streams being visible yet, an I/O unit
// pointed at the aggregate renegotiates its formats asynchronously and that
// is where readiness has to be awaited. Logged here for error attribution.
RTC_LOG(LS_INFO) << "Created aggregate device " << aggregate_device_id
<< " (streams at creation: input="
<< GetNumStreams(aggregate_device_id, true)
<< ", output=" << GetNumStreams(aggregate_device_id, false)
<< ")";

return aggregate_device_id;
}

bool DestroyAggregateDevice(AudioObjectID aggregate_device_id) {
OSStatus status = AudioHardwareDestroyAggregateDevice(aggregate_device_id);
if (status != noErr) {
RTC_LOG(LS_WARNING) << "AudioHardwareDestroyAggregateDevice failed: "
<< status;
return false;
}
return true;
}

} // namespace mac_audio_utils
} // namespace webrtc
11 changes: 11 additions & 0 deletions modules/audio_device/mac/audio_device_utils_mac.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,17 @@ bool IsInputDevice(AudioObjectID device_id);

bool IsOutputDevice(AudioObjectID device_id);

// Creates a private (process local) aggregate device combining the given
// output and input devices, so a single HAL I/O unit can address both. The
// output device is the clock master and drift compensation is enabled for
// the input sub device. Returns the aggregate AudioObjectID on success.
std::optional<AudioObjectID> CreatePrivateAggregateDevice(
AudioObjectID output_device_id,
AudioObjectID input_device_id);

// Destroys an aggregate device created by CreatePrivateAggregateDevice.
bool DestroyAggregateDevice(AudioObjectID aggregate_device_id);

} // namespace mac_audio_utils
} // namespace webrtc

Expand Down