Core Audio: Include format information in device enumeration.

This commit is contained in:
David Reid
2025-07-20 18:23:31 +10:00
parent e49fc58584
commit c62dc472eb
+231 -261
View File
@@ -35551,118 +35551,30 @@ static void ma_AVAudioSessionPortDescription_to_device_info(AVAudioSessionPortDe
}
#endif
static ma_result ma_context_enumerate_devices__coreaudio(ma_context* pContext, ma_enum_devices_callback_proc callback, void* pUserData)
{
#if defined(MA_APPLE_DESKTOP)
UInt32 deviceCount;
AudioObjectID* pDeviceObjectIDs;
AudioObjectID defaultDeviceObjectIDPlayback;
AudioObjectID defaultDeviceObjectIDCapture;
ma_result result;
UInt32 iDevice;
ma_find_default_AudioObjectID(pContext, ma_device_type_playback, &defaultDeviceObjectIDPlayback); /* OK if this fails. */
ma_find_default_AudioObjectID(pContext, ma_device_type_capture, &defaultDeviceObjectIDCapture); /* OK if this fails. */
result = ma_get_device_object_ids__coreaudio(pContext, &deviceCount, &pDeviceObjectIDs);
if (result != MA_SUCCESS) {
return result;
}
for (iDevice = 0; iDevice < deviceCount; ++iDevice) {
AudioObjectID deviceObjectID = pDeviceObjectIDs[iDevice];
ma_device_info info;
MA_ZERO_OBJECT(&info);
if (ma_get_AudioObject_uid(pContext, deviceObjectID, sizeof(info.id.coreaudio), info.id.coreaudio) != MA_SUCCESS) {
continue;
}
if (ma_get_AudioObject_name(pContext, deviceObjectID, sizeof(info.name), info.name) != MA_SUCCESS) {
continue;
}
if (ma_does_AudioObject_support_playback(pContext, deviceObjectID)) {
if (deviceObjectID == defaultDeviceObjectIDPlayback) {
info.isDefault = MA_TRUE;
}
if (!callback(ma_device_type_playback, &info, pUserData)) {
break;
}
}
if (ma_does_AudioObject_support_capture(pContext, deviceObjectID)) {
if (deviceObjectID == defaultDeviceObjectIDCapture) {
info.isDefault = MA_TRUE;
}
if (!callback(ma_device_type_capture, &info, pUserData)) {
break;
}
}
}
ma_free(pDeviceObjectIDs, &pContext->allocationCallbacks);
#else
ma_device_info info;
NSArray *pInputs = [[[AVAudioSession sharedInstance] currentRoute] inputs];
NSArray *pOutputs = [[[AVAudioSession sharedInstance] currentRoute] outputs];
for (AVAudioSessionPortDescription* pPortDesc in pOutputs) {
ma_AVAudioSessionPortDescription_to_device_info(pPortDesc, &info);
if (!callback(ma_device_type_playback, &info, pUserData)) {
return MA_SUCCESS;
}
}
for (AVAudioSessionPortDescription* pPortDesc in pInputs) {
ma_AVAudioSessionPortDescription_to_device_info(pPortDesc, &info);
if (!callback(ma_device_type_capture, &info, pUserData)) {
return MA_SUCCESS;
}
}
#endif
return MA_SUCCESS;
}
static ma_result ma_context_get_device_info__coreaudio(ma_context* pContext, ma_device_type deviceType, const ma_device_id* pDeviceID, ma_device_info* pDeviceInfo)
static ma_bool32 ma_context_enumerate_device_by_AudioObjectID__coreaudio(ma_context* pContext, AudioObjectID deviceObjectID, AudioObjectID defaultDeviceObjectID, ma_device_type deviceType, ma_enum_devices_callback_proc callback, void* pUserData)
{
ma_result result;
ma_device_info deviceInfo;
MA_ASSERT(pContext != NULL);
MA_ZERO_OBJECT(&deviceInfo);
#if defined(MA_APPLE_DESKTOP)
/* Desktop */
/* Default. */
if (deviceObjectID == defaultDeviceObjectID) {
deviceInfo.isDefault = MA_TRUE;
}
/* ID. */
if (ma_get_AudioObject_uid(pContext, deviceObjectID, sizeof(deviceInfo.id.coreaudio), deviceInfo.id.coreaudio) != MA_SUCCESS) {
return MA_TRUE;
}
/* Name. */
if (ma_get_AudioObject_name(pContext, deviceObjectID, sizeof(deviceInfo.name), deviceInfo.name) != MA_SUCCESS) {
return MA_TRUE;
}
/* Data Format. */
{
AudioObjectID deviceObjectID;
AudioObjectID defaultDeviceObjectID;
UInt32 streamDescriptionCount;
AudioStreamRangedDescription* pStreamDescriptions;
UInt32 iStreamDescription;
UInt32 sampleRateRangeCount;
AudioValueRange* pSampleRateRanges;
ma_find_default_AudioObjectID(pContext, deviceType, &defaultDeviceObjectID); /* OK if this fails. */
result = ma_find_AudioObjectID(pContext, deviceType, pDeviceID, &deviceObjectID);
if (result != MA_SUCCESS) {
return result;
}
result = ma_get_AudioObject_uid(pContext, deviceObjectID, sizeof(pDeviceInfo->id.coreaudio), pDeviceInfo->id.coreaudio);
if (result != MA_SUCCESS) {
return result;
}
result = ma_get_AudioObject_name(pContext, deviceObjectID, sizeof(pDeviceInfo->name), pDeviceInfo->name);
if (result != MA_SUCCESS) {
return result;
}
if (deviceObjectID == defaultDeviceObjectID) {
pDeviceInfo->isDefault = MA_TRUE;
}
/*
There could be a large number of permutations here. Fortunately there is only a single channel count
being reported which reduces this quite a bit. For sample rates we're only reporting those that are
@@ -35671,200 +35583,258 @@ static ma_result ma_context_get_device_info__coreaudio(ma_context* pContext, ma_
if some driver performs software data conversion and therefore reports every possible format and
sample rate.
*/
pDeviceInfo->nativeDataFormatCount = 0;
ma_result result;
ma_format uniqueFormats[ma_format_count];
ma_uint32 uniqueFormatCount = 0;
ma_uint32 channels;
UInt32 streamDescriptionCount;
AudioStreamRangedDescription* pStreamDescriptions;
UInt32 iStreamDescription;
deviceInfo.nativeDataFormatCount = 0;
/* Channels. */
result = ma_get_AudioObject_channel_count(pContext, deviceObjectID, deviceType, &channels);
if (result != MA_SUCCESS) {
return MA_TRUE; /* Failed to get channel count. */
}
/* Formats. */
{
ma_format uniqueFormats[ma_format_count];
ma_uint32 uniqueFormatCount = 0;
ma_uint32 channels;
result = ma_get_AudioObject_stream_descriptions(pContext, deviceObjectID, deviceType, &streamDescriptionCount, &pStreamDescriptions);
if (result != MA_SUCCESS) {
return MA_TRUE;
}
/* Channels. */
result = ma_get_AudioObject_channel_count(pContext, deviceObjectID, deviceType, &channels);
for (iStreamDescription = 0; iStreamDescription < streamDescriptionCount; ++iStreamDescription) {
ma_format format;
ma_bool32 hasFormatBeenHandled = MA_FALSE;
ma_uint32 iOutputFormat;
ma_uint32 iSampleRate;
UInt32 sampleRateRangeCount;
AudioValueRange* pSampleRateRanges;
result = ma_format_from_AudioStreamBasicDescription(&pStreamDescriptions[iStreamDescription].mFormat, &format);
if (result != MA_SUCCESS) {
return result;
continue;
}
/* Formats. */
result = ma_get_AudioObject_stream_descriptions(pContext, deviceObjectID, deviceType, &streamDescriptionCount, &pStreamDescriptions);
if (result != MA_SUCCESS) {
return result;
MA_ASSERT(format != ma_format_unknown);
/* Make sure the format isn't already in the output list. */
for (iOutputFormat = 0; iOutputFormat < uniqueFormatCount; ++iOutputFormat) {
if (uniqueFormats[iOutputFormat] == format) {
hasFormatBeenHandled = MA_TRUE;
break;
}
}
for (iStreamDescription = 0; iStreamDescription < streamDescriptionCount; ++iStreamDescription) {
ma_format format;
ma_bool32 hasFormatBeenHandled = MA_FALSE;
ma_uint32 iOutputFormat;
ma_uint32 iSampleRate;
/* If we've already handled this format just skip it. */
if (hasFormatBeenHandled) {
continue;
}
result = ma_format_from_AudioStreamBasicDescription(&pStreamDescriptions[iStreamDescription].mFormat, &format);
if (result != MA_SUCCESS) {
continue;
}
uniqueFormats[uniqueFormatCount] = format;
uniqueFormatCount += 1;
MA_ASSERT(format != ma_format_unknown);
/* Sample Rates */
result = ma_get_AudioObject_sample_rates(pContext, deviceObjectID, deviceType, &sampleRateRangeCount, &pSampleRateRanges);
if (result != MA_SUCCESS) {
return MA_TRUE; /* Failed to retrieve sample rates. */
}
/* Make sure the format isn't already in the output list. */
for (iOutputFormat = 0; iOutputFormat < uniqueFormatCount; ++iOutputFormat) {
if (uniqueFormats[iOutputFormat] == format) {
hasFormatBeenHandled = MA_TRUE;
break;
}
}
/*
Annoyingly Core Audio reports a sample rate range. We just get all the standard rates that are
between this range.
*/
for (iSampleRate = 0; iSampleRate < sampleRateRangeCount; ++iSampleRate) {
ma_uint32 iStandardSampleRate;
for (iStandardSampleRate = 0; iStandardSampleRate < ma_countof(g_maStandardSampleRatePriorities); iStandardSampleRate += 1) {
ma_uint32 standardSampleRate = g_maStandardSampleRatePriorities[iStandardSampleRate];
if (standardSampleRate >= pSampleRateRanges[iSampleRate].mMinimum && standardSampleRate <= pSampleRateRanges[iSampleRate].mMaximum) {
/* We have a new data format. Add it to the list. */
deviceInfo.nativeDataFormats[deviceInfo.nativeDataFormatCount].format = format;
deviceInfo.nativeDataFormats[deviceInfo.nativeDataFormatCount].channels = channels;
deviceInfo.nativeDataFormats[deviceInfo.nativeDataFormatCount].sampleRate = standardSampleRate;
deviceInfo.nativeDataFormats[deviceInfo.nativeDataFormatCount].flags = 0;
deviceInfo.nativeDataFormatCount += 1;
/* If we've already handled this format just skip it. */
if (hasFormatBeenHandled) {
continue;
}
uniqueFormats[uniqueFormatCount] = format;
uniqueFormatCount += 1;
/* Sample Rates */
result = ma_get_AudioObject_sample_rates(pContext, deviceObjectID, deviceType, &sampleRateRangeCount, &pSampleRateRanges);
if (result != MA_SUCCESS) {
return result;
}
/*
Annoyingly Core Audio reports a sample rate range. We just get all the standard rates that are
between this range.
*/
for (iSampleRate = 0; iSampleRate < sampleRateRangeCount; ++iSampleRate) {
ma_uint32 iStandardSampleRate;
for (iStandardSampleRate = 0; iStandardSampleRate < ma_countof(g_maStandardSampleRatePriorities); iStandardSampleRate += 1) {
ma_uint32 standardSampleRate = g_maStandardSampleRatePriorities[iStandardSampleRate];
if (standardSampleRate >= pSampleRateRanges[iSampleRate].mMinimum && standardSampleRate <= pSampleRateRanges[iSampleRate].mMaximum) {
/* We have a new data format. Add it to the list. */
pDeviceInfo->nativeDataFormats[pDeviceInfo->nativeDataFormatCount].format = format;
pDeviceInfo->nativeDataFormats[pDeviceInfo->nativeDataFormatCount].channels = channels;
pDeviceInfo->nativeDataFormats[pDeviceInfo->nativeDataFormatCount].sampleRate = standardSampleRate;
pDeviceInfo->nativeDataFormats[pDeviceInfo->nativeDataFormatCount].flags = 0;
pDeviceInfo->nativeDataFormatCount += 1;
if (pDeviceInfo->nativeDataFormatCount >= ma_countof(pDeviceInfo->nativeDataFormats)) {
break; /* No more room for any more formats. */
}
if (deviceInfo.nativeDataFormatCount >= ma_countof(deviceInfo.nativeDataFormats)) {
break; /* No more room for any more formats. */
}
}
}
ma_free(pSampleRateRanges, &pContext->allocationCallbacks);
if (pDeviceInfo->nativeDataFormatCount >= ma_countof(pDeviceInfo->nativeDataFormats)) {
break; /* No more room for any more formats. */
}
}
ma_free(pStreamDescriptions, &pContext->allocationCallbacks);
ma_free(pSampleRateRanges, &pContext->allocationCallbacks);
if (deviceInfo.nativeDataFormatCount >= ma_countof(deviceInfo.nativeDataFormats)) {
break; /* No more room for any more formats. */
}
}
ma_free(pStreamDescriptions, &pContext->allocationCallbacks);
}
return callback(deviceType, &deviceInfo, pUserData);
}
#else
/* Mobile */
{
ma_context_state_coreaudio* pContextStateCoreAudio = ma_context_get_backend_state__coreaudio(pContext);
AudioComponentDescription desc;
AudioComponent component;
AudioUnit audioUnit;
OSStatus status;
AudioUnitScope formatScope;
AudioUnitElement formatElement;
AudioStreamBasicDescription bestFormat;
UInt32 propSize;
static ma_bool32 ma_context_enumerate_device_by_AVAudioSessionPortDescription__coreaudio(ma_context* pContext, AVAudioSessionPortDescription* pPortDesc, ma_device_type deviceType, ma_enum_devices_callback_proc callback, void* pUserData)
{
ma_context_state_coreaudio* pContextStateCoreAudio = ma_context_get_backend_state__coreaudio(pContext);
ma_device_info deviceInfo;
AudioComponentDescription desc;
AudioComponent component;
AudioUnit audioUnit;
OSStatus status;
AudioUnitScope formatScope;
AudioUnitElement formatElement;
AudioStreamBasicDescription bestFormat;
UInt32 propSize;
/* We want to ensure we use a consistent device name to device enumeration. */
if (pDeviceID != NULL && pDeviceID->coreaudio[0] != '\0') {
ma_bool32 found = MA_FALSE;
if (deviceType == ma_device_type_playback) {
NSArray *pOutputs = [[[AVAudioSession sharedInstance] currentRoute] outputs];
for (AVAudioSessionPortDescription* pPortDesc in pOutputs) {
if (strcmp(pDeviceID->coreaudio, [pPortDesc.UID UTF8String]) == 0) {
ma_AVAudioSessionPortDescription_to_device_info(pPortDesc, pDeviceInfo);
found = MA_TRUE;
break;
}
}
} else {
NSArray *pInputs = [[[AVAudioSession sharedInstance] currentRoute] inputs];
for (AVAudioSessionPortDescription* pPortDesc in pInputs) {
if (strcmp(pDeviceID->coreaudio, [pPortDesc.UID UTF8String]) == 0) {
ma_AVAudioSessionPortDescription_to_device_info(pPortDesc, pDeviceInfo);
found = MA_TRUE;
break;
}
}
}
MA_ZERO_OBJECT(&deviceInfo);
if (!found) {
return MA_DOES_NOT_EXIST;
}
} else {
if (deviceType == ma_device_type_playback) {
ma_strncpy_s(pDeviceInfo->name, sizeof(pDeviceInfo->name), MA_DEFAULT_PLAYBACK_DEVICE_NAME, (size_t)-1);
} else {
ma_strncpy_s(pDeviceInfo->name, sizeof(pDeviceInfo->name), MA_DEFAULT_CAPTURE_DEVICE_NAME, (size_t)-1);
}
}
/* ID and Name. */
ma_AVAudioSessionPortDescription_to_device_info(pPortDesc, &deviceInfo);
/*
Default.
/*
Retrieving device information is more annoying on mobile than desktop. For simplicity I'm locking this down to whatever format is
reported on a temporary I/O unit. The problem, however, is that this doesn't return a value for the sample rate which we need to
retrieve from the AVAudioSession shared instance.
*/
desc.componentType = kAudioUnitType_Output;
desc.componentSubType = kAudioUnitSubType_RemoteIO;
desc.componentManufacturer = kAudioUnitManufacturer_Apple;
desc.componentFlags = 0;
desc.componentFlagsMask = 0;
I'm not sure how to do this. I'm going to just assume it's the default device for now. Advice welcome!
*/
deviceInfo.isDefault = MA_TRUE;
component = pContextStateCoreAudio->AudioComponentFindNext(NULL, &desc);
if (component == NULL) {
return MA_FAILED_TO_INIT_BACKEND;
}
/* Data Format. */
/*
Retrieving device information is more annoying on mobile than desktop. For simplicity I'm locking this down to whatever format is
reported on a temporary I/O unit. The problem, however, is that this doesn't return a value for the sample rate which we need to
retrieve from the AVAudioSession shared instance.
*/
desc.componentType = kAudioUnitType_Output;
desc.componentSubType = kAudioUnitSubType_RemoteIO;
desc.componentManufacturer = kAudioUnitManufacturer_Apple;
desc.componentFlags = 0;
desc.componentFlagsMask = 0;
status = pContextStateCoreAudio->AudioComponentInstanceNew(component, &audioUnit);
if (status != noErr) {
return ma_result_from_OSStatus(status);
}
component = pContextStateCoreAudio->AudioComponentFindNext(NULL, &desc);
if (component == NULL) {
return MA_TRUE;
}
formatScope = (deviceType == ma_device_type_playback) ? kAudioUnitScope_Input : kAudioUnitScope_Output;
formatElement = (deviceType == ma_device_type_playback) ? MA_COREAUDIO_OUTPUT_BUS : MA_COREAUDIO_INPUT_BUS;
status = pContextStateCoreAudio->AudioComponentInstanceNew(component, &audioUnit);
if (status != noErr) {
return MA_TRUE;
}
propSize = sizeof(bestFormat);
status = pContextStateCoreAudio->AudioUnitGetProperty(audioUnit, kAudioUnitProperty_StreamFormat, formatScope, formatElement, &bestFormat, &propSize);
if (status != noErr) {
pContextStateCoreAudio->AudioComponentInstanceDispose(audioUnit);
return ma_result_from_OSStatus(status);
}
formatScope = (deviceType == ma_device_type_playback) ? kAudioUnitScope_Input : kAudioUnitScope_Output;
formatElement = (deviceType == ma_device_type_playback) ? MA_COREAUDIO_OUTPUT_BUS : MA_COREAUDIO_INPUT_BUS;
propSize = sizeof(bestFormat);
status = pContextStateCoreAudio->AudioUnitGetProperty(audioUnit, kAudioUnitProperty_StreamFormat, formatScope, formatElement, &bestFormat, &propSize);
if (status != noErr) {
pContextStateCoreAudio->AudioComponentInstanceDispose(audioUnit);
audioUnit = NULL;
return MA_TRUE;
}
/* Only a single format is being reported for iOS. */
pDeviceInfo->nativeDataFormatCount = 1;
pContextStateCoreAudio->AudioComponentInstanceDispose(audioUnit);
audioUnit = NULL;
result = ma_format_from_AudioStreamBasicDescription(&bestFormat, &pDeviceInfo->nativeDataFormats[0].format);
/* Only a single format is being reported for iOS. */
deviceInfo.nativeDataFormatCount = 1;
result = ma_format_from_AudioStreamBasicDescription(&bestFormat, &deviceInfo.nativeDataFormats[0].format);
if (result != MA_SUCCESS) {
return MA_TRUE;
}
deviceInfo.nativeDataFormats[0].channels = bestFormat.mChannelsPerFrame;
/*
It looks like Apple are wanting to push the whole AVAudioSession thing. Thus, we need to use that to determine device settings. To do
this we just get the shared instance and inspect.
*/
@autoreleasepool {
AVAudioSession* pAudioSession = [AVAudioSession sharedInstance];
MA_ASSERT(pAudioSession != NULL);
deviceInfo.nativeDataFormats[0].sampleRate = (ma_uint32)pAudioSession.sampleRate;
}
return callback(deviceType, &deviceInfo, pUserData);
}
#endif
static ma_result ma_context_enumerate_devices__coreaudio(ma_context* pContext, ma_enum_devices_callback_proc callback, void* pUserData)
{
#if defined(MA_APPLE_DESKTOP)
{
UInt32 deviceCount;
AudioObjectID* pDeviceObjectIDs;
AudioObjectID defaultDeviceObjectIDPlayback;
AudioObjectID defaultDeviceObjectIDCapture;
ma_result result;
UInt32 iDevice;
ma_find_default_AudioObjectID(pContext, ma_device_type_playback, &defaultDeviceObjectIDPlayback); /* OK if this fails. */
ma_find_default_AudioObjectID(pContext, ma_device_type_capture, &defaultDeviceObjectIDCapture); /* OK if this fails. */
result = ma_get_device_object_ids__coreaudio(pContext, &deviceCount, &pDeviceObjectIDs);
if (result != MA_SUCCESS) {
return result;
}
pDeviceInfo->nativeDataFormats[0].channels = bestFormat.mChannelsPerFrame;
for (iDevice = 0; iDevice < deviceCount; ++iDevice) {
AudioObjectID deviceObjectID = pDeviceObjectIDs[iDevice];
/*
It looks like Apple are wanting to push the whole AVAudioSession thing. Thus, we need to use that to determine device settings. To do
this we just get the shared instance and inspect.
*/
@autoreleasepool {
AVAudioSession* pAudioSession = [AVAudioSession sharedInstance];
MA_ASSERT(pAudioSession != NULL);
if (ma_does_AudioObject_support_playback(pContext, deviceObjectID)) {
ma_bool32 cbResult = ma_context_enumerate_device_by_AudioObjectID__coreaudio(pContext, deviceObjectID, defaultDeviceObjectIDPlayback, ma_device_type_playback, callback, pUserData);
if (cbResult == MA_FALSE) {
break;
}
}
pDeviceInfo->nativeDataFormats[0].sampleRate = (ma_uint32)pAudioSession.sampleRate;
if (ma_does_AudioObject_support_capture(pContext, deviceObjectID)) {
ma_bool32 cbResult = ma_context_enumerate_device_by_AudioObjectID__coreaudio(pContext, deviceObjectID, defaultDeviceObjectIDCapture, ma_device_type_capture, callback, pUserData);
if (cbResult == MA_FALSE) {
break;
}
}
}
ma_free(pDeviceObjectIDs, &pContext->allocationCallbacks);
}
#else
{
NSArray *pInputs = [[[AVAudioSession sharedInstance] currentRoute] inputs];
NSArray *pOutputs = [[[AVAudioSession sharedInstance] currentRoute] outputs];
for (AVAudioSessionPortDescription* pPortDesc in pOutputs) {
ma_AVAudioSessionPortDescription_to_device_info(pPortDesc, &info);
/* I'm not sure how to check for default devices. I'm just going to assume the first one is the default. */
if (pPortDesc == pOutputs[0]) {
info.isDefault = MA_TRUE;
}
if (!callback(ma_device_type_playback, &info, pUserData)) {
return MA_SUCCESS;
}
}
for (AVAudioSessionPortDescription* pPortDesc in pInputs) {
ma_AVAudioSessionPortDescription_to_device_info(pPortDesc, &info);
/* I'm not sure how to check for default devices. I'm just going to assume the first one is the default. */
if (pPortDesc == pInputs[0]) {
info.isDefault = MA_TRUE;
}
if (!callback(ma_device_type_capture, &info, pUserData)) {
return MA_SUCCESS;
}
}
}
#endif
#endif
(void)pDeviceInfo; /* Unused. */
return MA_SUCCESS;
}
@@ -37134,7 +37104,7 @@ static ma_device_backend_vtable ma_gDeviceBackendVTable_CoreAudio =
ma_context_init__coreaudio,
ma_context_uninit__coreaudio,
ma_context_enumerate_devices__coreaudio,
ma_context_get_device_info__coreaudio,
NULL,
ma_device_init__coreaudio,
ma_device_uninit__coreaudio,
ma_device_start__coreaudio,