diff --git a/mini_al.h b/mini_al.h index 7a5fb2d9..d324806c 100644 --- a/mini_al.h +++ b/mini_al.h @@ -648,7 +648,7 @@ typedef union int sdl; // SDL devices are identified with an index. #endif #ifdef MAL_SUPPORT_NULL - int nullbackend; // Always 0. + int nullbackend; // The null backend uses an integer for device IDs. #endif } mal_device_id; @@ -791,10 +791,22 @@ typedef struct } jack; } mal_context_config; +typedef mal_bool32 (* mal_enum_devices_callback_proc)(mal_context* pContext, mal_device_type type, const mal_device_info* pInfo, void* pUserData); + struct mal_context { - mal_backend backend; // DirectSound, ALSA, etc. + mal_backend backend; // DirectSound, ALSA, etc. mal_context_config config; + mal_mutex deviceEnumLock; // Used to make mal_context_get_devices() thread safe. + mal_mutex deviceInfoLock; // Used to make mal_context_get_device_info() thread safe. + mal_uint32 deviceInfoCapacity; // Total capacity of pDeviceInfos. + mal_uint32 playbackDeviceInfoCount; + mal_uint32 captureDeviceInfoCount; + mal_device_info* pDeviceInfos; // Playback devices first, then capture. + + mal_result (* onEnumDevices )(mal_context* pContext, mal_enum_devices_callback_proc callback, void* pUserData); // Return false from the callback to stop enumeration. + mal_result (* onGetDeviceInfo)(mal_context* pContext, mal_device_type type, const mal_device_id* pDeviceID, mal_device_info* pDeviceInfo); + mal_bool32 (* onDeviceIDEqual)(mal_context* pContext, const mal_device_id* pID0, const mal_device_id* pID1); union { @@ -1140,7 +1152,7 @@ struct mal_device mal_format format; mal_uint32 channels; mal_uint32 sampleRate; - mal_uint8 channelMap[MAL_MAX_CHANNELS]; + mal_channel channelMap[MAL_MAX_CHANNELS]; mal_uint32 bufferSizeInFrames; mal_uint32 periods; mal_uint32 state; @@ -1166,7 +1178,7 @@ struct mal_device mal_format internalFormat; mal_uint32 internalChannels; mal_uint32 internalSampleRate; - mal_uint8 internalChannelMap[MAL_MAX_CHANNELS]; + mal_channel internalChannelMap[MAL_MAX_CHANNELS]; mal_dsp dsp; // Samples run through this to convert samples to a format suitable for use by the backend. mal_uint32 _dspFrameCount; // Internal use only. Used when running the device -> DSP -> client pipeline. See mal_device__on_read_from_device(). const mal_uint8* _dspFrames; // ^^^ AS ABOVE ^^^ @@ -1351,6 +1363,65 @@ mal_result mal_context_init(const mal_backend backends[], mal_uint32 backendCoun // Thread Safety: UNSAFE mal_result mal_context_uninit(mal_context* pContext); +// Enumerates over every device (both playback and capture). +// +// This is a lower-level enumeration function to the easier to use mal_context_get_devices(). Use +// this if you would rather not incur an internal heap allocation, or it simply suits you program +// better. +// +// Do _not_ assume the first device in the returned lists are the default devices. +// +// Some backends and platforms may only support default playback and capture devices. +// +// Note that this only retrieves the ID and name/description of the device. The reason for only +// retrieving basic information is that it would otherwise require opening the backend device in +// order to probe it for more detailed information which can be inefficient. Consider using +// mal_context_get_device_info() for this, but do _not_ call it in the enumeration callback or +// else you can end up in a deadlock. In general, you should not do anything complicated from +// within the callback. +// +// Consider using mal_context_get_devices() for a simpler and safer API. +// +// Returning false from the callback will stop enumeration. +// +// Return Value: +// MAL_SUCCESS if successful; any other error code otherwise. +// +// Thread Safety: SAFE +// This is guarded using a simple mutex lock. You cannot call mal_context_get_device_info() from +// within the callback because otherwise you can end up in a deadlock. +mal_result mal_context_enumerate_devices(mal_context* pContext, mal_enum_devices_callback_proc callback, void* pUserData); + +// Retrieves basic information about every active playback and/or capture device. +// +// You can pass in NULL for the playback or capture lists in which case they'll be ignored. +// +// The returned pointers will become invalid upon the next call this this function, or when the +// context is uninitialized. Do not free the returned pointers. +// +// This function follows the same enumeration rules as mal_context_enumerate_devices(). See +// documentation for mal_context_enumerate_devices() for more information. +// +// Return Value: +// MAL_SUCCESS if successful; any other error code otherwise. +// +// Thread Safety: SAFE +// Since each call to this function invalidates the pointers from the previous call, you +// should not be calling this simultaneously across multiple threads. Instead, you need to +// make a copy of the returned data with your own higher level synchronization. +mal_result mal_context_get_devices(mal_context* pContext, mal_device_info** ppPlaybackDeviceInfos, mal_uint32* pPlaybackDeviceCount, mal_device_info** ppCaptureDeviceInfos, mal_uint32* pCaptureDeviceCount); + +// Retrieves information about a device with the given ID. +// +// Do _not_ call this from within the mal_context_enumerate_devices_callback(). +// +// Return Value: +// MAL_SUCCESS if successful; any other error code otherwise. +// +// Thread Safety: SAFE +// This is guarded using a simple mutex lock. +mal_result mal_context_get_device_info(mal_context* pContext, mal_device_type type, const mal_device_id* pDeviceID, mal_device_info* pDeviceInfo); + // Enumerates over each device of the given type (playback or capture). // // It is _not_ safe to assume the first enumerated device is the default device. @@ -2099,6 +2170,9 @@ static mal_uint32 g_malStandardSampleRatePriorities[] = { MAL_SAMPLE_RATE_384000 }; +#define MAL_DEFAULT_PLAYBACK_DEVICE_NAME "Default Playback Device" +#define MAL_DEFAULT_CAPTURE_DEVICE_NAME "Default Capture Device" + /////////////////////////////////////////////////////////////////////////////// // // Standard Library Stuff @@ -3129,6 +3203,19 @@ static inline mal_uint32 mal_device__get_state(mal_device* pDevice) #endif +static mal_bool32 mal_context__device_id_equal(mal_context* pContext, const mal_device_id* pID0, const mal_device_id* pID1) +{ + mal_assert(pContext != NULL); + + if (pID0 == pID1) return MAL_TRUE; + + if (pContext->onDeviceIDEqual) { + return pContext->onDeviceIDEqual(pContext, pID0, pID1); + } + + return MAL_FALSE; +} + // Generic function for retrieving the name of a device by it's ID. // // This function simply enumerates every device and then retrieves the name of the first device that has the same ID. @@ -4185,10 +4272,10 @@ static mal_result mal_enumerate_devices__wasapi(mal_context* pContext, mal_devic if (infoSize > 0) { if (type == mal_device_type_playback) { mal_copy_memory(pInfo->id.wasapi, &MAL_IID_DEVINTERFACE_AUDIO_RENDER, sizeof(MAL_IID_DEVINTERFACE_AUDIO_RENDER)); - mal_strncpy_s(pInfo->name, sizeof(pInfo->name), "Default Playback Device", (size_t)-1); + mal_strncpy_s(pInfo->name, sizeof(pInfo->name), MAL_DEFAULT_PLAYBACK_DEVICE_NAME, (size_t)-1); } else { mal_copy_memory(pInfo->id.wasapi, &MAL_IID_DEVINTERFACE_AUDIO_CAPTURE, sizeof(MAL_IID_DEVINTERFACE_AUDIO_CAPTURE)); - mal_strncpy_s(pInfo->name, sizeof(pInfo->name), "Default Capture Device", (size_t)-1); + mal_strncpy_s(pInfo->name, sizeof(pInfo->name), MAL_DEFAULT_CAPTURE_DEVICE_NAME, (size_t)-1); } pInfo += 1; @@ -9687,9 +9774,9 @@ mal_result mal_enumerate_devices__jack(mal_context* pContext, mal_device_type ty if (infoSize > 0) { pInfo[0].id.jack = 0; if (type == mal_device_type_playback) { - mal_strncpy_s(pInfo[0].name, sizeof(pInfo[0].name), "Default Playback Device", (size_t)-1); + mal_strncpy_s(pInfo[0].name, sizeof(pInfo[0].name), MAL_DEFAULT_PLAYBACK_DEVICE_NAME, (size_t)-1); } else { - mal_strncpy_s(pInfo[0].name, sizeof(pInfo[0].name), "Default Capture Device", (size_t)-1); + mal_strncpy_s(pInfo[0].name, sizeof(pInfo[0].name), MAL_DEFAULT_CAPTURE_DEVICE_NAME, (size_t)-1); } *pCount = 1; @@ -10069,9 +10156,9 @@ static mal_result mal_enumerate_devices__oss(mal_context* pContext, mal_device_t if (infoSize > 0) { mal_strncpy_s(pInfo[0].id.oss, sizeof(pInfo[0].id.oss), "/dev/dsp", (size_t)-1); if (type == mal_device_type_playback) { - mal_strncpy_s(pInfo[0].name, sizeof(pInfo[0].name), "Default Playback Device", (size_t)-1); + mal_strncpy_s(pInfo[0].name, sizeof(pInfo[0].name), MAL_DEFAULT_PLAYBACK_DEVICE_NAME, (size_t)-1); } else { - mal_strncpy_s(pInfo[0].name, sizeof(pInfo[0].name), "Default Capture Device", (size_t)-1); + mal_strncpy_s(pInfo[0].name, sizeof(pInfo[0].name), MAL_DEFAULT_CAPTURE_DEVICE_NAME, (size_t)-1); } *pCount = 1; @@ -10584,10 +10671,10 @@ return_default_device: if (infoSize > 0) { if (type == mal_device_type_playback) { pInfo->id.opensl = SL_DEFAULTDEVICEID_AUDIOOUTPUT; - mal_strncpy_s(pInfo->name, sizeof(pInfo->name), "Default Playback Device", (size_t)-1); + mal_strncpy_s(pInfo->name, sizeof(pInfo->name), MAL_DEFAULT_PLAYBACK_DEVICE_NAME, (size_t)-1); } else { pInfo->id.opensl = SL_DEFAULTDEVICEID_AUDIOINPUT; - mal_strncpy_s(pInfo->name, sizeof(pInfo->name), "Default Capture Device", (size_t)-1); + mal_strncpy_s(pInfo->name, sizeof(pInfo->name), MAL_DEFAULT_CAPTURE_DEVICE_NAME, (size_t)-1); } } } @@ -11403,10 +11490,10 @@ mal_result mal_enumerate_devices__openal(mal_context* pContext, mal_device_type if (infoSize > 0) { if (type == mal_device_type_playback) { pInfo->id.sdl = 0; - mal_strncpy_s(pInfo->name, sizeof(pInfo->name), "Default Playback Device", (size_t)-1); + mal_strncpy_s(pInfo->name, sizeof(pInfo->name), MAL_DEFAULT_PLAYBACK_DEVICE_NAME, (size_t)-1); } else { pInfo->id.sdl = 0; - mal_strncpy_s(pInfo->name, sizeof(pInfo->name), "Default Capture Device", (size_t)-1); + mal_strncpy_s(pInfo->name, sizeof(pInfo->name), MAL_DEFAULT_CAPTURE_DEVICE_NAME, (size_t)-1); } pInfo += 1; @@ -12030,10 +12117,10 @@ mal_result mal_enumerate_devices__sdl(mal_context* pContext, mal_device_type typ // SDL1 uses default devices. if (type == mal_device_type_playback) { pInfo->id.sdl = 0; - mal_strncpy_s(pInfo->name, sizeof(pInfo->name), "Default Playback Device", (size_t)-1); + mal_strncpy_s(pInfo->name, sizeof(pInfo->name), MAL_DEFAULT_PLAYBACK_DEVICE_NAME, (size_t)-1); } else { pInfo->id.sdl = 0; - mal_strncpy_s(pInfo->name, sizeof(pInfo->name), "Default Capture Device", (size_t)-1); + mal_strncpy_s(pInfo->name, sizeof(pInfo->name), MAL_DEFAULT_CAPTURE_DEVICE_NAME, (size_t)-1); } pInfo += 1; @@ -12750,11 +12837,21 @@ mal_result mal_context_init(const mal_backend backends[], mal_uint32 backendCoun // If this iteration was successful, return. if (result == MAL_SUCCESS) { + result = mal_mutex_init(pContext, &pContext->deviceEnumLock); + if (result != MAL_SUCCESS) { + mal_context_post_error(pContext, NULL, "WARNING: Failed to initialize mutex for device enumeration. mal_context_get_devices() is not thread safe.", MAL_FAILED_TO_CREATE_MUTEX); + } + result = mal_mutex_init(pContext, &pContext->deviceInfoLock); + if (result != MAL_SUCCESS) { + mal_context_post_error(pContext, NULL, "WARNING: Failed to initialize mutex for device info retrieval. mal_context_get_device_info() is not thread safe.", MAL_FAILED_TO_CREATE_MUTEX); + } + pContext->backend = backend; return result; } } + // If we get here it means an error occurred. mal_zero_object(pContext); // Safety. return MAL_NO_BACKEND; } @@ -12835,12 +12932,196 @@ mal_result mal_context_uninit(mal_context* pContext) } mal_context_uninit_backend_apis(pContext); + mal_mutex_uninit(&pContext->deviceEnumLock); mal_assert(MAL_FALSE); return MAL_NO_BACKEND; } +mal_result mal_context_enumerate_devices(mal_context* pContext, mal_enum_devices_callback_proc callback, void* pUserData) +{ + if (pContext == NULL || pContext->onEnumDevices == NULL || callback == NULL) return MAL_INVALID_ARGS; + + mal_result result; + mal_mutex_lock(&pContext->deviceEnumLock); + { + result = pContext->onEnumDevices(pContext, callback, pUserData); + } + mal_mutex_unlock(&pContext->deviceEnumLock); + + return result; +} + + +mal_bool32 mal_context_get_devices__enum_callback(mal_context* pContext, mal_device_type type, const mal_device_info* pInfo, void* pUserData) +{ + (void)pUserData; + + // We need to insert the device info into our main internal buffer. Where it goes depends on the device type. If it's a capture device + // it's just appended to the end. If it's a playback device it's inserted just before the first capture device. + + // First make sure we have room. Since the number of devices we add to the list is usually relatively small I've decided to use a + // simple fixed size increment for buffer expansion. + const mal_uint32 bufferExpansionCount = 2; + const mal_uint32 totalDeviceInfoCount = pContext->playbackDeviceInfoCount + pContext->captureDeviceInfoCount; + + if (pContext->deviceInfoCapacity >= totalDeviceInfoCount) { + mal_uint32 newCapacity = totalDeviceInfoCount + bufferExpansionCount; + mal_device_info* pNewInfos = (mal_device_info*)mal_realloc(pContext->pDeviceInfos, sizeof(*pContext->pDeviceInfos)*newCapacity); + if (pNewInfos == NULL) { + return MAL_FALSE; // Out of memory. + } + + pContext->pDeviceInfos = pNewInfos; + pContext->deviceInfoCapacity = newCapacity; + } + + if (type == mal_device_type_playback) { + // Playback. Insert just before the first capture device. + + // The first thing to do is move all of the capture devices down a slot. + mal_uint32 iFirstCaptureDevice = pContext->playbackDeviceInfoCount; + for (size_t iCaptureDevice = totalDeviceInfoCount; iCaptureDevice > iFirstCaptureDevice; --iCaptureDevice) { + pContext->pDeviceInfos[iCaptureDevice] = pContext->pDeviceInfos[iCaptureDevice-1]; + } + + // Now just insert where the first capture device was before moving it down a slot. + pContext->pDeviceInfos[iFirstCaptureDevice] = *pInfo; + pContext->playbackDeviceInfoCount += 1; + } else { + // Capture. Insert at the end. + pContext->pDeviceInfos[totalDeviceInfoCount] = *pInfo; + pContext->captureDeviceInfoCount += 1; + } + + return MAL_TRUE; +} + +mal_result mal_context_get_devices(mal_context* pContext, mal_device_info** ppPlaybackDeviceInfos, mal_uint32* pPlaybackDeviceCount, mal_device_info** ppCaptureDeviceInfos, mal_uint32* pCaptureDeviceCount) +{ + // Safety. + if (ppPlaybackDeviceInfos != NULL) *ppPlaybackDeviceInfos = NULL; + if (pPlaybackDeviceCount != NULL) *pPlaybackDeviceCount = 0; + if (ppCaptureDeviceInfos != NULL) *ppCaptureDeviceInfos = NULL; + if (pCaptureDeviceCount != NULL) *pCaptureDeviceCount = 0; + + if (pContext == NULL || pContext->onEnumDevices == NULL) return MAL_INVALID_ARGS; + + // Note that we don't use mal_context_enumerate_devices() here because we want to do locking at a higher level. + mal_result result; + mal_mutex_lock(&pContext->deviceEnumLock); + { + // Reset everything first. + pContext->playbackDeviceInfoCount = 0; + pContext->captureDeviceInfoCount = 0; + + // Now enumerate over available devices. + result = pContext->onEnumDevices(pContext, mal_context_get_devices__enum_callback, NULL); + if (result == MAL_SUCCESS) { + // Playback devices. + if (ppPlaybackDeviceInfos != NULL) { + *ppPlaybackDeviceInfos = pContext->pDeviceInfos; + } + if (pPlaybackDeviceCount != NULL) { + *pPlaybackDeviceCount = pContext->playbackDeviceInfoCount; + } + + // Capture devices. + if (ppCaptureDeviceInfos != NULL) { + *ppCaptureDeviceInfos = pContext->pDeviceInfos + pContext->playbackDeviceInfoCount; // Capture devices come after playback devices. + } + if (pCaptureDeviceCount != NULL) { + *pCaptureDeviceCount = pContext->captureDeviceInfoCount; + } + } + } + mal_mutex_unlock(&pContext->deviceEnumLock); + + return result; +} + + +typedef struct +{ + mal_device_type type; + const mal_device_id* pDeviceID; + mal_device_info* pDeviceInfo; +} mal_context_get_device_info__enum_callback_data; + +static mal_bool32 mal_context_get_device_info__enum_callback(mal_context* pContext, mal_device_type type, const mal_device_info* pInfoIn, void* pUserData) +{ + mal_context_get_device_info__enum_callback_data* pData = (mal_context_get_device_info__enum_callback_data*)pUserData; + mal_assert(pData != NULL); + + if (type == pData->type && mal_context__device_id_equal(pContext, &pInfoIn->id, pData->pDeviceID)) { + // Found it. Copy the info and stop iteration. + if (pData->pDeviceInfo != NULL) { + *pData->pDeviceInfo = *pInfoIn; + } + + return MAL_FALSE; // Stop iteration. + } else { + // Didn't find it. Continue. + return MAL_TRUE; + } +} + +mal_result mal_context_get_device_info(mal_context* pContext, mal_device_type type, const mal_device_id* pDeviceID, mal_device_info* pDeviceInfo) +{ + // Safety. + if (pDeviceInfo != NULL) { + mal_zero_object(pDeviceInfo); + } + + if (pContext == NULL) return MAL_INVALID_ARGS; + + // The backend may have an optimized device info retrieval function. If so, try that first. + if (pContext->onGetDeviceInfo != NULL) { + mal_result result; + mal_mutex_lock(&pContext->deviceInfoLock); + { + result = pContext->onGetDeviceInfo(pContext, type, pDeviceID, pDeviceInfo); + } + mal_mutex_unlock(&pContext->deviceInfoLock); + + if (result == MAL_SUCCESS) { + return MAL_SUCCESS; + } + } + + // If we get here it means the backend does not have a method for retrieving detailed information about the + // device. In this case we fall back to retrieving just basic information. To do this we enumerate over the + // devices. If the device ID is null we just use a simple default name. This is where the potential for a + // deadlock comes into play. + if (pDeviceID != NULL) { + mal_result result = MAL_NO_DEVICE; + + if (pContext->onEnumDevices == NULL) { + return MAL_NO_DEVICE; + } + + mal_context_get_device_info__enum_callback_data data; + data.type = type; + data.pDeviceID = pDeviceID; + data.pDeviceInfo = pDeviceInfo; + return mal_context_enumerate_devices(pContext, mal_context_get_device_info__enum_callback, &data); + } else { + // It's asking for the default device. We don't have a way to retrieve advanced info so we just stick + // with the name. + if (pDeviceInfo != NULL) { + if (type == mal_device_type_playback) { + mal_strcpy_s(pDeviceInfo->name, sizeof(pDeviceInfo->name), MAL_DEFAULT_PLAYBACK_DEVICE_NAME); + } else { + mal_strcpy_s(pDeviceInfo->name, sizeof(pDeviceInfo->name), MAL_DEFAULT_CAPTURE_DEVICE_NAME); + } + } + + return MAL_SUCCESS; + } +} + + mal_result mal_enumerate_devices(mal_context* pContext, mal_device_type type, mal_uint32* pCount, mal_device_info* pInfo) { if (pCount == NULL) return mal_post_error(NULL, "mal_enumerate_devices() called with invalid arguments (pCount == 0).", MAL_INVALID_ARGS); @@ -13123,9 +13404,9 @@ mal_result mal_device_init(mal_context* pContext, mal_device_type type, mal_devi // We failed to get the device name, so fall back to some generic names. if (pDeviceID == NULL) { if (type == mal_device_type_playback) { - mal_strncpy_s(pDevice->name, sizeof(pDevice->name), "Default Playback Device", (size_t)-1); + mal_strncpy_s(pDevice->name, sizeof(pDevice->name), MAL_DEFAULT_PLAYBACK_DEVICE_NAME, (size_t)-1); } else { - mal_strncpy_s(pDevice->name, sizeof(pDevice->name), "Default Capture Device", (size_t)-1); + mal_strncpy_s(pDevice->name, sizeof(pDevice->name), MAL_DEFAULT_CAPTURE_DEVICE_NAME, (size_t)-1); } } else { if (type == mal_device_type_playback) {