SDL: coreaudio: Make sure device handles are unique.

From 2fd94476703002514e339fba876800b605692406 Mon Sep 17 00:00:00 2001
From: "Ryan C. Gordon" <[EMAIL REDACTED]>
Date: Fri, 29 Mar 2024 00:01:49 -0400
Subject: [PATCH] coreaudio: Make sure device handles are unique.

AudioDeviceID is not unique (hardware that can do both capture and output
will expose both interfaces off the same AudioDeviceID!).
---
 src/audio/SDL_audio.c               |  3 ++
 src/audio/coreaudio/SDL_coreaudio.m | 64 +++++++++++++++++++++--------
 2 files changed, 49 insertions(+), 18 deletions(-)

diff --git a/src/audio/SDL_audio.c b/src/audio/SDL_audio.c
index a2468e8c2c2f3..5ca394dbec14a 100644
--- a/src/audio/SDL_audio.c
+++ b/src/audio/SDL_audio.c
@@ -594,6 +594,9 @@ static SDL_AudioDevice *CreateAudioOutputDevice(const char *name, const SDL_Audi
 // The audio backends call this when a new device is plugged in.
 SDL_AudioDevice *SDL_AddAudioDevice(const SDL_bool iscapture, const char *name, const SDL_AudioSpec *inspec, void *handle)
 {
+    // device handles MUST be unique! If the target reuses the same handle for hardware with both input and output interfaces, wrap it in a pointer you SDL_malloc'd!
+    SDL_assert(SDL_FindPhysicalAudioDeviceByHandle(handle) == NULL);
+
     const SDL_AudioFormat default_format = iscapture ? DEFAULT_AUDIO_CAPTURE_FORMAT : DEFAULT_AUDIO_OUTPUT_FORMAT;
     const int default_channels = iscapture ? DEFAULT_AUDIO_CAPTURE_CHANNELS : DEFAULT_AUDIO_OUTPUT_CHANNELS;
     const int default_freq = iscapture ? DEFAULT_AUDIO_CAPTURE_FREQUENCY : DEFAULT_AUDIO_OUTPUT_FREQUENCY;
diff --git a/src/audio/coreaudio/SDL_coreaudio.m b/src/audio/coreaudio/SDL_coreaudio.m
index d58269639d22d..17cfe79f6707b 100644
--- a/src/audio/coreaudio/SDL_coreaudio.m
+++ b/src/audio/coreaudio/SDL_coreaudio.m
@@ -42,6 +42,28 @@
 #endif
 
 #ifdef MACOSX_COREAUDIO
+// Apparently AudioDeviceID values might not be unique, so we wrap it in an SDL_malloc()'d pointer
+//  to make it so. Use FindCoreAudioDeviceByHandle to deal with this redirection, if you need to
+//  map from an AudioDeviceID to a SDL handle.
+typedef struct SDLCoreAudioHandle
+{
+    AudioDeviceID devid;
+    SDL_bool iscapture;
+} SDLCoreAudioHandle;
+
+static SDL_bool TestCoreAudioDeviceHandleCallback(SDL_AudioDevice *device, void *handle)
+{
+    const SDLCoreAudioHandle *a = (const SDLCoreAudioHandle *) device->handle;
+    const SDLCoreAudioHandle *b = (const SDLCoreAudioHandle *) handle;
+    return (a->devid == b->devid) && (!!a->iscapture == !!b->iscapture);
+}
+
+static SDL_AudioDevice *FindCoreAudioDeviceByHandle(const AudioDeviceID devid, const SDL_bool iscapture)
+{
+    SDLCoreAudioHandle handle = { devid, iscapture };
+    return SDL_FindPhysicalAudioDeviceByCallback(TestCoreAudioDeviceHandleCallback, &handle);
+}
+
 static const AudioObjectPropertyAddress devlist_address = {
     kAudioHardwarePropertyDevices,
     kAudioObjectPropertyScopeGlobal,
@@ -70,7 +92,7 @@
 static OSStatus DeviceAliveNotification(AudioObjectID devid, UInt32 num_addr, const AudioObjectPropertyAddress *addrs, void *data)
 {
     SDL_AudioDevice *device = (SDL_AudioDevice *)data;
-    SDL_assert(((AudioObjectID)(size_t)device->handle) == devid);
+    SDL_assert(((const SDLCoreAudioHandle *) device->handle)->devid == devid);
 
     UInt32 alive = 1;
     UInt32 size = sizeof(alive);
@@ -95,8 +117,9 @@ static OSStatus DeviceAliveNotification(AudioObjectID devid, UInt32 num_addr, co
 
 static void COREAUDIO_FreeDeviceHandle(SDL_AudioDevice *device)
 {
-    const AudioDeviceID devid = (AudioDeviceID)(size_t)device->handle;
-    AudioObjectRemovePropertyListener(devid, &alive_address, DeviceAliveNotification, device);
+    SDLCoreAudioHandle *handle = (SDLCoreAudioHandle *) device->handle;
+    AudioObjectRemovePropertyListener(handle->devid, &alive_address, DeviceAliveNotification, device);
+    SDL_free(handle);
 }
 
 // This only _adds_ new devices. Removal is handled by devices triggering kAudioDevicePropertyDeviceIsAlive property changes.
@@ -117,8 +140,7 @@ static void RefreshPhysicalDevices(void)
 
     const UInt32 total_devices = (UInt32) (size / sizeof(AudioDeviceID));
     for (UInt32 i = 0; i < total_devices; i++) {
-        SDL_AudioDevice *device = SDL_FindPhysicalAudioDeviceByHandle((void *)((size_t)devs[i]));
-        if (device) {
+        if (FindCoreAudioDeviceByHandle(devs[i], SDL_TRUE) || FindCoreAudioDeviceByHandle(devs[i], SDL_FALSE)) {
             devs[i] = 0;  // The system and SDL both agree it's already here, don't check it again.
         }
     }
@@ -206,10 +228,16 @@ static void RefreshPhysicalDevices(void)
                        ((iscapture) ? "capture" : "output"),
                        (int)i, name, (int)dev);
                 #endif
-
-                SDL_AudioDevice *device = SDL_AddAudioDevice(iscapture ? SDL_TRUE : SDL_FALSE, name, &spec, (void *)((size_t)dev));
-                if (device) {
-                    AudioObjectAddPropertyListener(dev, &alive_address, DeviceAliveNotification, device);
+                SDLCoreAudioHandle *newhandle = (SDLCoreAudioHandle *) SDL_calloc(1, sizeof (*newhandle));
+                if (newhandle) {
+                    newhandle->devid = dev;
+                    newhandle->iscapture = iscapture ? SDL_TRUE : SDL_FALSE;
+                    SDL_AudioDevice *device = SDL_AddAudioDevice(newhandle->iscapture, name, &spec, newhandle);
+                    if (device) {
+                        AudioObjectAddPropertyListener(dev, &alive_address, DeviceAliveNotification, device);
+                    } else {
+                        SDL_free(newhandle);
+                    }
                 }
             }
             SDL_free(name); // SDL_AddAudioDevice() would have copied the string.
@@ -226,12 +254,12 @@ static OSStatus DeviceListChangedNotification(AudioObjectID systemObj, UInt32 nu
     return noErr;
 }
 
-static OSStatus DefaultAudioDeviceChangedNotification(AudioObjectID inObjectID, const AudioObjectPropertyAddress *addr)
+static OSStatus DefaultAudioDeviceChangedNotification(const SDL_bool iscapture, AudioObjectID inObjectID, const AudioObjectPropertyAddress *addr)
 {
     AudioDeviceID devid;
     UInt32 size = sizeof(devid);
     if (AudioObjectGetPropertyData(inObjectID, addr, 0, NULL, &size, &devid) == noErr) {
-        SDL_DefaultAudioDeviceChanged(SDL_FindPhysicalAudioDeviceByHandle((void *)((size_t)devid)));
+        SDL_DefaultAudioDeviceChanged(FindCoreAudioDeviceByHandle(devid, iscapture));
     }
     return noErr;
 }
@@ -242,7 +270,7 @@ static OSStatus DefaultOutputDeviceChangedNotification(AudioObjectID inObjectID,
     SDL_Log("COREAUDIO: default output device changed!");
     #endif
     SDL_assert(inNumberAddresses == 1);
-    return DefaultAudioDeviceChangedNotification(inObjectID, inAddresses);
+    return DefaultAudioDeviceChangedNotification(SDL_FALSE, inObjectID, inAddresses);
 }
 
 static OSStatus DefaultInputDeviceChangedNotification(AudioObjectID inObjectID, UInt32 inNumberAddresses, const AudioObjectPropertyAddress *inAddresses, void *inUserData)
@@ -251,7 +279,7 @@ static OSStatus DefaultInputDeviceChangedNotification(AudioObjectID inObjectID,
     SDL_Log("COREAUDIO: default input device changed!");
     #endif
     SDL_assert(inNumberAddresses == 1);
-    return DefaultAudioDeviceChangedNotification(inObjectID, inAddresses);
+    return DefaultAudioDeviceChangedNotification(SDL_TRUE, inObjectID, inAddresses);
 }
 
 static void COREAUDIO_DetectDevices(SDL_AudioDevice **default_output, SDL_AudioDevice **default_capture)
@@ -266,7 +294,7 @@ static void COREAUDIO_DetectDevices(SDL_AudioDevice **default_output, SDL_AudioD
 
     size = sizeof(AudioDeviceID);
     if (AudioObjectGetPropertyData(kAudioObjectSystemObject, &default_output_device_address, 0, NULL, &size, &devid) == noErr) {
-        SDL_AudioDevice *device = SDL_FindPhysicalAudioDeviceByHandle((void *)((size_t)devid));
+        SDL_AudioDevice *device = FindCoreAudioDeviceByHandle(devid, SDL_FALSE);
         if (device) {
             *default_output = device;
         }
@@ -275,7 +303,7 @@ static void COREAUDIO_DetectDevices(SDL_AudioDevice **default_output, SDL_AudioD
 
     size = sizeof(AudioDeviceID);
     if (AudioObjectGetPropertyData(kAudioObjectSystemObject, &default_input_device_address, 0, NULL, &size, &devid) == noErr) {
-        SDL_AudioDevice *device = SDL_FindPhysicalAudioDeviceByHandle((void *)((size_t)devid));
+        SDL_AudioDevice *device = FindCoreAudioDeviceByHandle(devid, SDL_TRUE);
         if (device) {
             *default_capture = device;
         }
@@ -631,10 +659,10 @@ static void COREAUDIO_CloseDevice(SDL_AudioDevice *device)
 #ifdef MACOSX_COREAUDIO
 static int PrepareDevice(SDL_AudioDevice *device)
 {
-    void *handle = device->handle;
-    SDL_assert(handle != NULL);  // this meant "system default" in SDL2, but doesn't anymore
+    SDL_assert(device->handle != NULL);  // this meant "system default" in SDL2, but doesn't anymore
 
-    const AudioDeviceID devid = (AudioDeviceID)((size_t)handle);
+    const SDLCoreAudioHandle *handle = (const SDLCoreAudioHandle *) device->handle;
+    const AudioDeviceID devid = handle->devid;
     OSStatus result = noErr;
     UInt32 size = 0;