Просмотр исходного кода

audio: Added SDL_GetAudioDeviceProperties() and a unique-id property for it.

In addition to adding properties to audio devices, the new property,
`SDL_PROP_AUDIO_DEVICE_UNIQUE_ID_STRING`, allows a best-effort option to find
the same specific audio device used on a previous run of a program, even in an
imperfect way. Most backends don't support this, but CoreAudio on macOS and
IMMDevice on Windows can.

coreaudio: Rework device enumeration queries, acquire unique_id.

- No longer keeps multiple AudioObjectPropertyAddress objects, just change the
  selector for each query.
- Move string retrieval for AudioObjectProperties off to a separate function.
- Query for unique_id.
- If a device name is blank, don't reject the device, try to come up with a
  name of some sort (either the unique_id or "Unnamed audio device").

windows: Add unique audio device identifiers to IMMDevice support.

This uses the new "StableId" property on Windows 11 if available, and the
device id (IMMDevice::GetId()) otherwise.

Microsoft claims that GetId() remains valid between reboots and even
disconnecting and reconnecting the audio device (although I am dubious that is
universally true when plugging into a different USB port, etc), but driver and
OS updates might change the identifier. StableId goes further, promising that
software updates won't change it, either.

This adds support to our WASAPI and DirectSound backends, if you're on at
least Windows Vista. Windows XP will not offer a unique ID for devices.

Fixes #16258.
Ryan C. Gordon 5 дней назад
Родитель
Сommit
0ccb0bf626

+ 45 - 0
include/SDL3/SDL_audio.h

@@ -658,6 +658,51 @@ extern SDL_DECLSPEC bool SDLCALL SDL_GetAudioDeviceFormat(SDL_AudioDeviceID devi
  */
 extern SDL_DECLSPEC int * SDLCALL SDL_GetAudioDeviceChannelMap(SDL_AudioDeviceID devid, int *count);
 
+/**
+ * Get the properties associated with an audio device.
+ *
+ * This can be used with both logical and physical devices. Note that while
+ * apps can hang any data they want here, physical device IDs are global; it
+ * would be better to assign data to one's own logical device so it doesn't
+ * interfere with other parts of the program that might be using the same
+ * physical device ID.
+ *
+ * Properties provided by SDL for physical devices will also be made available
+ * on their associated logical devices, unless otherwise noted.
+ *
+ * The application can hang any data it wants here, but the following
+ * properties are understood by SDL:
+ *
+ * - `SDL_PROP_AUDIO_DEVICE_UNIQUE_ID_STRING`: This identifier can be used to
+ *   locate a specific device. In optimal conditions, this identifier will not
+ *   change between runs of an app, hardware disconnection, and system reboots.
+ *   However, depending on the hardware, operating system, and other
+ *   circumstances, a device's identifier may change, so if the app cannot
+ *   find a device with a previously queried identifier, the user should be
+ *   prompted to choose a new device (possibly the same device, now with a new
+ *   identifier). Device identifier strings have no specific format, the
+ *   format may change in the future without warning, and are likely different
+ *   between different operating systems on the same hardware. If the system
+ *   cannot reasonably provide a unique identifier, this property will not be
+ *   set. Note that property is useful for finding specific hardware again on
+ *   a later run of the app, but often times it's better to just open the
+ *   default device (SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK or
+ *   SDL_AUDIO_DEVICE_DEFAULT_RECORDING), and let the user set this up globally
+ *   on their platform.
+ *
+ * \param devid the audio device instance id to query.
+ * \returns a valid property ID on success or 0 on failure; call
+ *          SDL_GetError() for more information.
+ *
+ * \threadsafety It is safe to call this function from any thread.
+ *
+ * \since This function is available since SDL 3.6.0.
+ */
+extern SDL_DECLSPEC SDL_PropertiesID SDLCALL SDL_GetAudioDeviceProperties(SDL_AudioDeviceID devid);
+
+#define SDL_PROP_AUDIO_DEVICE_UNIQUE_ID_STRING "SDL.audio.device.unique_id"
+
+
 /**
  * Open a specific audio device.
  *

+ 59 - 9
src/audio/SDL_audio.c

@@ -575,6 +575,11 @@ static void DestroyLogicalAudioDevice(SDL_LogicalAudioDevice *logdev)
     }
 
     UpdateAudioStreamFormatsPhysical(logdev->physical_device);
+
+    if (logdev->props) {
+        SDL_DestroyProperties(logdev->props);
+    }
+
     SDL_free(logdev);
 }
 
@@ -597,10 +602,15 @@ static void DestroyPhysicalAudioDevice(SDL_AudioDevice *device)
 
     SDL_UnlockMutex(device->lock);  // don't use ReleaseAudioDevice because we don't want to change refcounts while destroying.
 
+    if (device->props) {
+        SDL_DestroyProperties(device->props);
+    }
+
     SDL_DestroyMutex(device->lock);
     SDL_DestroyCondition(device->close_cond);
     SDL_free(device->work_buffer);
     SDL_free(device->chmap);
+    SDL_free(device->unique_id);
     SDL_free(device->name);
     SDL_free(device);
 }
@@ -624,7 +634,7 @@ void RefPhysicalAudioDevice(SDL_AudioDevice *device)
     SDL_AtomicIncRef(&device->refcount);
 }
 
-static SDL_AudioDevice *CreatePhysicalAudioDevice(const char *name, bool recording, const SDL_AudioSpec *spec, void *handle, SDL_AtomicInt *device_count)
+static SDL_AudioDevice *CreatePhysicalAudioDevice(const char *name, const char *unique_id, bool recording, const SDL_AudioSpec *spec, void *handle, SDL_AtomicInt *device_count)
 {
     SDL_assert(name != NULL);
 
@@ -646,8 +656,18 @@ static SDL_AudioDevice *CreatePhysicalAudioDevice(const char *name, bool recordi
         return NULL;
     }
 
+    if (unique_id) {
+        device->unique_id = SDL_strdup(unique_id);
+        if (!device->unique_id) {
+            SDL_free(device->name);
+            SDL_free(device);
+            return NULL;
+        }
+    }
+
     device->lock = SDL_CreateMutex();
     if (!device->lock) {
+        SDL_free(device->unique_id);
         SDL_free(device->name);
         SDL_free(device);
         return NULL;
@@ -656,6 +676,7 @@ static SDL_AudioDevice *CreatePhysicalAudioDevice(const char *name, bool recordi
     device->close_cond = SDL_CreateCondition();
     if (!device->close_cond) {
         SDL_DestroyMutex(device->lock);
+        SDL_free(device->unique_id);
         SDL_free(device->name);
         SDL_free(device);
         return NULL;
@@ -678,6 +699,7 @@ static SDL_AudioDevice *CreatePhysicalAudioDevice(const char *name, bool recordi
     } else {
         SDL_DestroyCondition(device->close_cond);
         SDL_DestroyMutex(device->lock);
+        SDL_free(device->unique_id);
         SDL_free(device->name);
         SDL_free(device);
         device = NULL;
@@ -688,19 +710,19 @@ static SDL_AudioDevice *CreatePhysicalAudioDevice(const char *name, bool recordi
     return device;
 }
 
-static SDL_AudioDevice *CreateAudioRecordingDevice(const char *name, const SDL_AudioSpec *spec, void *handle)
+static SDL_AudioDevice *CreateAudioRecordingDevice(const char *name, const char *unique_id, const SDL_AudioSpec *spec, void *handle)
 {
     SDL_assert(current_audio.impl.HasRecordingSupport);
-    return CreatePhysicalAudioDevice(name, true, spec, handle, &current_audio.recording_device_count);
+    return CreatePhysicalAudioDevice(name, unique_id, true, spec, handle, &current_audio.recording_device_count);
 }
 
-static SDL_AudioDevice *CreateAudioPlaybackDevice(const char *name, const SDL_AudioSpec *spec, void *handle)
+static SDL_AudioDevice *CreateAudioPlaybackDevice(const char *name, const char *unique_id, const SDL_AudioSpec *spec, void *handle)
 {
-    return CreatePhysicalAudioDevice(name, false, spec, handle, &current_audio.playback_device_count);
+    return CreatePhysicalAudioDevice(name, unique_id, false, spec, handle, &current_audio.playback_device_count);
 }
 
 // The audio backends call this when a new device is plugged in.
-SDL_AudioDevice *SDL_AddAudioDevice(bool recording, const char *name, const SDL_AudioSpec *inspec, void *handle)
+SDL_AudioDevice *SDL_AddAudioDevice(bool recording, const char *name, const char *unique_id, const SDL_AudioSpec *inspec, void *handle)
 {
     // device handles MUST be unique! If the target reuses the same handle for hardware with both recording and playback interfaces, wrap it in a pointer you SDL_malloc'd!
     SDL_assert(SDL_FindPhysicalAudioDeviceByHandle(handle) == NULL);
@@ -721,7 +743,7 @@ SDL_AudioDevice *SDL_AddAudioDevice(bool recording, const char *name, const SDL_
         spec.freq = (inspec->freq != 0) ? inspec->freq : default_freq;
     }
 
-    SDL_AudioDevice *device = recording ? CreateAudioRecordingDevice(name, &spec, handle) : CreateAudioPlaybackDevice(name, &spec, handle);
+    SDL_AudioDevice *device = recording ? CreateAudioRecordingDevice(name, unique_id, &spec, handle) : CreateAudioPlaybackDevice(name, unique_id, &spec, handle);
 
     // Add a device add event to the pending list, to be pushed when the event queue is pumped (away from any of our internal threads).
     if (device) {
@@ -869,9 +891,9 @@ static void SDL_AudioDetectDevices_Default(SDL_AudioDevice **default_playback, S
     SDL_assert(current_audio.impl.OnlyHasDefaultPlaybackDevice);
     SDL_assert(current_audio.impl.OnlyHasDefaultRecordingDevice || !current_audio.impl.HasRecordingSupport);
 
-    *default_playback = SDL_AddAudioDevice(false, DEFAULT_PLAYBACK_DEVNAME, NULL, (void *)((size_t)0x1));
+    *default_playback = SDL_AddAudioDevice(false, DEFAULT_PLAYBACK_DEVNAME, NULL, NULL, (void *)((size_t)0x1));
     if (current_audio.impl.HasRecordingSupport) {
-        *default_recording = SDL_AddAudioDevice(true, DEFAULT_RECORDING_DEVNAME, NULL, (void *)((size_t)0x2));
+        *default_recording = SDL_AddAudioDevice(true, DEFAULT_RECORDING_DEVNAME, NULL, NULL, (void *)((size_t)0x2));
     }
 }
 
@@ -1668,6 +1690,34 @@ int *SDL_GetAudioDeviceChannelMap(SDL_AudioDeviceID devid, int *count)
     return result;
 }
 
+SDL_PropertiesID SDL_GetAudioDeviceProperties(SDL_AudioDeviceID devid)
+{
+    SDL_AudioDevice *device = NULL;
+    SDL_LogicalAudioDevice *logdev = NULL;
+    SDL_PropertiesID props = 0;
+
+    if (SDL_IsAudioDeviceLogical(devid)) {
+        logdev = ObtainLogicalAudioDevice(devid, &device);
+    } else {
+        device = ObtainPhysicalAudioDevice(devid);
+    }
+
+    if (device) {  // found a device?
+        SDL_PropertiesID *propsptr = logdev ? &logdev->props : &device->props;
+        props = *propsptr;
+        if (!props) {
+            props = *propsptr = SDL_CreateProperties();
+            if (props != 0) {  // fill in some basic properties.
+                SDL_SetStringProperty(props, SDL_PROP_AUDIO_DEVICE_UNIQUE_ID_STRING, device->unique_id);
+            }
+        }
+
+        ReleaseAudioDevice(device);
+    }
+
+    return props;
+}
+
 
 // this is awkward, but this makes sure we can release the device lock
 //  so the device thread can terminate but also not have two things

+ 1 - 1
src/audio/SDL_audiodev.c

@@ -63,7 +63,7 @@ static void test_device(const bool recording, const char *fname, int flags, bool
                  * information,  making this information inaccessible at
                  * enumeration time
                  */
-                SDL_AddAudioDevice(recording, fname, NULL, (void *)(uintptr_t)dummyhandle);
+                SDL_AddAudioDevice(recording, fname, NULL, NULL, (void *)(uintptr_t)dummyhandle);
             }
         } else {
             close(audio_fd);

+ 12 - 1
src/audio/SDL_sysaudio.h

@@ -71,7 +71,7 @@ extern void SDL_SetupAudioResampler(void);
 /* Backends should call this as devices are added to the system (such as
    a USB headset being plugged in), and should also be called for
    for every device found during DetectDevices(). */
-extern SDL_AudioDevice *SDL_AddAudioDevice(bool recording, const char *name, const SDL_AudioSpec *spec, void *handle);
+extern SDL_AudioDevice *SDL_AddAudioDevice(bool recording, const char *name, const char *unique_id, const SDL_AudioSpec *spec, void *handle);
 
 /* Backends should call this if an opened audio device is lost.
    This can happen due to i/o errors, or a device being unplugged, etc. */
@@ -275,6 +275,9 @@ struct SDL_LogicalAudioDevice
     // App-supplied pointer for postmix callback.
     void *postmix_userdata;
 
+    // Properties, maybe copied from physical device.
+    SDL_PropertiesID props;
+
     // double-linked list of opened devices on the same physical device.
     SDL_LogicalAudioDevice *next;
     SDL_LogicalAudioDevice *prev;
@@ -302,6 +305,11 @@ struct SDL_AudioDevice
     // human-readable name of the device. ("SoundBlaster Pro 16")
     char *name;
 
+    // unique, platform-specific, backend-specific string to identify this specific device.
+    // It needs to survive between runs of the program, but ideally it survives reboots and
+    // device disconnect/reconnect, if possible. If it can't be provided, leave this NULL.
+    char *unique_id;
+
     // the unique instance ID of this device.
     SDL_AudioDeviceID instance_id;
 
@@ -352,6 +360,9 @@ struct SDL_AudioDevice
     // true if this physical device is currently opened by the backend.
     bool currently_opened;
 
+    // Properties!
+    SDL_PropertiesID props;
+
     // Data private to this driver
     struct SDL_PrivateAudioData *hidden;
 

+ 3 - 3
src/audio/alsa/SDL_alsa_audio.c

@@ -1364,7 +1364,7 @@ static int hotplug_device_process(snd_ctl_t *ctl, snd_ctl_card_info_t *ctl_card_
             adev->device_index = dev_idx;
             adev->recording = (direction == SND_PCM_STREAM_CAPTURE);
 
-            if (SDL_AddAudioDevice(recording, adev->name, NULL, adev) == NULL) {
+            if (SDL_AddAudioDevice(recording, adev->name, NULL, NULL, adev) == NULL) {
                 SDL_small_free(pcm_info, isstack);
                 SDL_free(adev->id);
                 SDL_free(adev->name);
@@ -1592,10 +1592,10 @@ static void ALSA_DetectDevices(SDL_AudioDevice **default_playback, SDL_AudioDevi
     bool has_default_playback = false, has_default_recording = false;
     ALSA_HotplugIteration(&has_default_playback, &has_default_recording); // run once now before a thread continues to check.
     if (has_default_playback) {
-        *default_playback = SDL_AddAudioDevice(/*recording=*/false, "ALSA default playback device", NULL, (void *)&default_playback_handle);
+        *default_playback = SDL_AddAudioDevice(/*recording=*/false, "ALSA default playback device", NULL, NULL, (void *)&default_playback_handle);
     }
     if (has_default_recording) {
-        *default_recording = SDL_AddAudioDevice(/*recording=*/true, "ALSA default recording device", NULL, (void *)&default_recording_handle);
+        *default_recording = SDL_AddAudioDevice(/*recording=*/true, "ALSA default recording device", NULL, NULL, (void *)&default_recording_handle);
     }
 
     if (!ALSA_start_udev()) {

+ 57 - 43
src/audio/coreaudio/SDL_coreaudio.m

@@ -122,6 +122,31 @@ static void COREAUDIO_FreeDeviceHandle(SDL_AudioDevice *device)
     SDL_free(handle);
 }
 
+static char *GetAudioDeviceStringProperty(AudioDeviceID dev, const AudioObjectPropertyAddress *addr)
+{
+    CFStringRef cfstr = NULL;
+    UInt32 size = sizeof(CFStringRef);
+    if (AudioObjectGetPropertyData(dev, addr, 0, NULL, &size, &cfstr) != kAudioHardwareNoError) {
+        return NULL;
+    }
+
+    CFIndex len = CFStringGetMaximumSizeForEncoding(CFStringGetLength(cfstr), kCFStringEncodingUTF8);
+    char *retval = (char *)SDL_malloc(len + 1);
+    if (!retval) {
+        return NULL;
+    }
+
+    const bool failed = !CFStringGetCString(cfstr, retval, len + 1, kCFStringEncodingUTF8);
+    CFRelease(cfstr);
+    if (failed) {
+        SDL_free(retval);
+        return NULL;
+    }
+
+    return retval;
+}
+
+
 // This only _adds_ new devices. Removal is handled by devices triggering kAudioDevicePropertyDeviceIsAlive property changes.
 static void RefreshPhysicalDevices(void)
 {
@@ -147,18 +172,8 @@ static void RefreshPhysicalDevices(void)
 
     // any non-zero items remaining in `devs` are new devices to be added.
     for (int recording = 0; recording < 2; recording++) {
-        const AudioObjectPropertyAddress addr = {
-            kAudioDevicePropertyStreamConfiguration,
-            recording ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput,
-            kAudioObjectPropertyElementMain
-        };
-        const AudioObjectPropertyAddress nameaddr = {
-            kAudioObjectPropertyName,
-            recording ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput,
-            kAudioObjectPropertyElementMain
-        };
-        const AudioObjectPropertyAddress freqaddr = {
-            kAudioDevicePropertyNominalSampleRate,
+        AudioObjectPropertyAddress addr = {
+            0,
             recording ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput,
             kAudioObjectPropertyElementMain
         };
@@ -172,6 +187,7 @@ static void RefreshPhysicalDevices(void)
             AudioBufferList *buflist = NULL;
             double sampleRate = 0;
 
+            addr.mSelector = kAudioDevicePropertyStreamConfiguration;
             if (AudioObjectGetPropertyDataSize(dev, &addr, 0, NULL, &size) != noErr) {
                 continue;
             } else if ((buflist = (AudioBufferList *)SDL_malloc(size)) == NULL) {
@@ -195,50 +211,48 @@ static void RefreshPhysicalDevices(void)
             }
 
             size = sizeof(sampleRate);
-            if (AudioObjectGetPropertyData(dev, &freqaddr, 0, NULL, &size, &sampleRate) == noErr) {
+            addr.mSelector = kAudioDevicePropertyNominalSampleRate;
+            if (AudioObjectGetPropertyData(dev, &addr, 0, NULL, &size, &sampleRate) == noErr) {
                 spec.freq = (int)sampleRate;
             }
 
-            CFStringRef cfstr = NULL;
-            size = sizeof(CFStringRef);
-            if (AudioObjectGetPropertyData(dev, &nameaddr, 0, NULL, &size, &cfstr) != kAudioHardwareNoError) {
+            addr.mSelector = kAudioObjectPropertyName;
+            char *name = GetAudioDeviceStringProperty(dev, &addr);
+            if (!name) {
                 continue;
             }
 
-            CFIndex len = CFStringGetMaximumSizeForEncoding(CFStringGetLength(cfstr), kCFStringEncodingUTF8);
-            char *name = (char *)SDL_malloc(len + 1);
-            bool usable = ((name != NULL) && (CFStringGetCString(cfstr, name, len + 1, kCFStringEncodingUTF8)));
+            addr.mSelector = kAudioDevicePropertyDeviceUID;
+            char *unique_id = GetAudioDeviceStringProperty(dev, &addr);  // it's okay if this one fails.
 
-            CFRelease(cfstr);
+            // Some devices have whitespace at the end...trim it.
+            int len = SDL_strlen(name);
+            while ((len > 0) && (name[len - 1] == ' ')) {
+                len--;
+            }
+            name[len] = '\0';
 
-            if (usable) {
-                // Some devices have whitespace at the end...trim it.
-                len = (CFIndex) SDL_strlen(name);
-                while ((len > 0) && (name[len - 1] == ' ')) {
-                    len--;
-                }
-                usable = (len > 0);
+            if (len == 0) {  // name is blank?!
+                SDL_free(name);
+                name = (unique_id && *unique_id) ? SDL_strdup(unique_id) : NULL;
             }
 
-            if (usable) {
-                name[len] = '\0';
-
-                #if DEBUG_COREAUDIO
-                SDL_Log("COREAUDIO: Found %s device #%d: '%s' (devid %d)", ((recording) ? "recording" : "playback"), (int)i, name, (int)dev);
-                #endif
-                SDLCoreAudioHandle *newhandle = (SDLCoreAudioHandle *) SDL_calloc(1, sizeof (*newhandle));
-                if (newhandle) {
-                    newhandle->devid = dev;
-                    newhandle->recording = recording ? true : false;
-                    SDL_AudioDevice *device = SDL_AddAudioDevice(newhandle->recording, name, &spec, newhandle);
-                    if (device) {
-                        AudioObjectAddPropertyListener(dev, &alive_address, DeviceAliveNotification, device);
-                    } else {
-                        SDL_free(newhandle);
-                    }
+            #if DEBUG_COREAUDIO
+            SDL_Log("COREAUDIO: Found %s device #%d: '%s' (devid %d, unique_id='%s')", ((recording) ? "recording" : "playback"), (int)i, name, (int)dev, unique_id);
+            #endif
+            SDLCoreAudioHandle *newhandle = (SDLCoreAudioHandle *) SDL_calloc(1, sizeof (*newhandle));
+            if (newhandle) {
+                newhandle->devid = dev;
+                newhandle->recording = recording ? true : false;
+                SDL_AudioDevice *device = SDL_AddAudioDevice(newhandle->recording, name ? name : "Unnamed audio device", unique_id, &spec, newhandle);
+                if (device) {
+                    AudioObjectAddPropertyListener(dev, &alive_address, DeviceAliveNotification, device);
+                } else {
+                    SDL_free(newhandle);
                 }
             }
             SDL_free(name); // SDL_AddAudioDevice() would have copied the string.
+            SDL_free(unique_id); // SDL_AddAudioDevice() would have copied the string.
         }
     }
 

+ 4 - 5
src/audio/directsound/SDL_directsound.c

@@ -185,11 +185,10 @@ static BOOL CALLBACK FindAllDevs(LPGUID guid, LPCWSTR desc, LPCWSTR module, LPVO
             if (cpyguid) {
                 SDL_copyp(cpyguid, guid);
 
-                /* Note that spec is NULL, because we are required to connect to the
-                 * device before getting the channel mask and output format, making
-                 * this information inaccessible at enumeration time
-                 */
-                SDL_AudioDevice *device = SDL_AddAudioDevice(data->recording, str, NULL, cpyguid);
+                // Note that spec is NULL, because we are required to connect to the
+                // device before getting the channel mask and output format, making
+                // this information inaccessible at enumeration time
+                SDL_AudioDevice *device = SDL_AddAudioDevice(data->recording, str, NULL, NULL, cpyguid);
                 if (device && data->default_device && data->default_device_guid) {
                     if (SDL_memcmp(cpyguid, data->default_device_guid, sizeof (GUID)) == 0) {
                         *data->default_device = device;

+ 2 - 2
src/audio/disk/SDL_diskaudio.c

@@ -155,8 +155,8 @@ static bool DISKAUDIO_OpenDevice(SDL_AudioDevice *device)
 
 static void DISKAUDIO_DetectDevices(SDL_AudioDevice **default_playback, SDL_AudioDevice **default_recording)
 {
-    *default_playback = SDL_AddAudioDevice(false, DEFAULT_PLAYBACK_DEVNAME, NULL, (void *)0x1);
-    *default_recording = SDL_AddAudioDevice(true, DEFAULT_RECORDING_DEVNAME, NULL, (void *)0x2);
+    *default_playback = SDL_AddAudioDevice(false, DEFAULT_PLAYBACK_DEVNAME, NULL, NULL, (void *)0x1);
+    *default_recording = SDL_AddAudioDevice(true, DEFAULT_RECORDING_DEVNAME, NULL, NULL, (void *)0x2);
 }
 
 static bool DISKAUDIO_Init(SDL_AudioDriverImpl *impl)

+ 2 - 2
src/audio/pipewire/SDL_pipewire.c

@@ -291,7 +291,7 @@ static bool io_list_check_add(struct io_node *node)
     spa_list_append(&hotplug_io_list, &node->link);
 
     if (hotplug_events_enabled) {
-        SDL_AddAudioDevice(node->recording, node->name, &node->spec, PW_ID_TO_HANDLE(node->id));
+        SDL_AddAudioDevice(node->recording, node->name, NULL, &node->spec, PW_ID_TO_HANDLE(node->id));
     }
 
     return true;
@@ -876,7 +876,7 @@ static void PIPEWIRE_DetectDevices(SDL_AudioDevice **default_playback, SDL_Audio
     }
 
     spa_list_for_each (io, &hotplug_io_list, link) {
-        SDL_AudioDevice *device = SDL_AddAudioDevice(io->recording, io->name, &io->spec, PW_ID_TO_HANDLE(io->id));
+        SDL_AudioDevice *device = SDL_AddAudioDevice(io->recording, io->name, NULL, &io->spec, PW_ID_TO_HANDLE(io->id));
         if (pipewire_default_sink_id && SDL_strcmp(io->path, pipewire_default_sink_id) == 0) {
             if (!io->recording) {
                 *default_playback = device;

+ 1 - 1
src/audio/pulseaudio/SDL_pulseaudio.c

@@ -854,7 +854,7 @@ static void AddPulseAudioDevice(const bool recording, const char *description, c
             SDL_free(handle);
         } else {
             handle->device_index = index;
-            SDL_AddAudioDevice(recording, description, &spec, handle);
+            SDL_AddAudioDevice(recording, description, NULL, &spec, handle);
         }
     }
 }

+ 1 - 1
src/audio/qnx/SDL_qsa_audio.c

@@ -383,7 +383,7 @@ static void QSA_DetectDevices(SDL_AudioDevice **default_playback, SDL_AudioDevic
                         SDL_assert(card <= 0xFFFF);
                         SDL_assert(deviceno <= 0xFFFF);
                         const Uint32 sdlhandle = ((Uint32) card) | (((Uint32) deviceno) << 16);
-                        SDL_AddAudioDevice(recording, fullname, pspec, (void *) ((size_t) sdlhandle));
+                        SDL_AddAudioDevice(recording, fullname, NULL, pspec, (void *) ((size_t) sdlhandle));
                     }
                 } else {
                     // Check if we got end of devices list

+ 3 - 2
src/audio/sndio/SDL_sndioaudio.c

@@ -330,8 +330,9 @@ static void SNDIO_Deinitialize(void)
 
 static void SNDIO_DetectDevices(SDL_AudioDevice **default_playback, SDL_AudioDevice **default_recording)
 {
-    *default_playback = SDL_AddAudioDevice(false, DEFAULT_PLAYBACK_DEVNAME, NULL, (void *)0x1);
-    *default_recording = SDL_AddAudioDevice(true, DEFAULT_RECORDING_DEVNAME, NULL, (void *)0x2);
+    // !!! FIXME: shouldn't we just use OnlyHasDefaultPlaybackDevice/OnlyHasDefaultRecordingDevice?
+    *default_playback = SDL_AddAudioDevice(false, DEFAULT_PLAYBACK_DEVNAME, NULL, NULL, (void *)0x1);
+    *default_recording = SDL_AddAudioDevice(true, DEFAULT_RECORDING_DEVNAME, NULL, NULL, (void *)0x2);
 }
 
 static bool SNDIO_Init(SDL_AudioDriverImpl *impl)

+ 1 - 1
src/core/android/SDL_android.c

@@ -817,7 +817,7 @@ JNIEXPORT void JNICALL SDL_JAVA_AUDIO_INTERFACE(nativeAddAudioDevice)(JNIEnv *en
         void *handle = (void *)((size_t)device_id);
         if (!SDL_FindPhysicalAudioDeviceByHandle(handle)) {
             const char *utf8name = (*env)->GetStringUTFChars(env, name, NULL);
-            SDL_AddAudioDevice(recording, SDL_strdup(utf8name), NULL, handle);
+            SDL_AddAudioDevice(recording, SDL_strdup(utf8name), NULL, NULL, handle);
             (*env)->ReleaseStringUTFChars(env, name, utf8name);
         }
     }

+ 24 - 8
src/core/windows/SDL_immdevice.c

@@ -54,6 +54,7 @@ static const IID SDL_IID_IMMEndpoint = { 0x1be09788, 0x6894, 0x4089,{ 0x85, 0x86
 static const PROPERTYKEY SDL_PKEY_Device_FriendlyName = { { 0xa45c254e, 0xdf1c, 0x4efd,{ 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, } }, 14 };
 static const PROPERTYKEY SDL_PKEY_AudioEngine_DeviceFormat = { { 0xf19f064d, 0x82c, 0x4e27,{ 0xbc, 0x73, 0x68, 0x82, 0xa1, 0xbb, 0x8e, 0x4c, } }, 0 };
 static const PROPERTYKEY SDL_PKEY_AudioEndpoint_GUID = { { 0x1da5d803, 0xd492, 0x4edd,{ 0x8c, 0x23, 0xe0, 0xc0, 0xff, 0xee, 0x7f, 0x0e, } }, 4 };
+static const PROPERTYKEY SDL_PKEY_AudioEndpoint_StableId = { { 0x1da5d803, 0xd492, 0x4edd,{ 0x8c, 0x23, 0xe0, 0xc0, 0xff, 0xee, 0x7f, 0x0e, } }, 12 };
 /* *INDENT-ON* */ // clang-format on
 
 static bool FindByDevIDCallback(SDL_AudioDevice *device, void *userdata)
@@ -83,13 +84,15 @@ LPCWSTR SDL_IMMDevice_GetDevID(SDL_AudioDevice *device)
     return (device && device->handle) ? ((const SDL_IMMDevice_HandleData *) device->handle)->immdevice_id : NULL;
 }
 
-static void GetMMDeviceInfo(IMMDevice *device, char **utf8dev, WAVEFORMATEXTENSIBLE *fmt, GUID *guid)
+static void GetMMDeviceInfo(IMMDevice *device, char **utf8dev, WAVEFORMATEXTENSIBLE *fmt, GUID *guid, char **unique_id)
 {
     /* PKEY_Device_FriendlyName gives you "Speakers (SoundBlaster Pro)" which drives me nuts. I'd rather it be
        "SoundBlaster Pro (Speakers)" but I guess that's developers vs users. Windows uses the FriendlyName in
        its own UIs, like Volume Control, etc. */
+    LPWSTR devid = NULL;
     IPropertyStore *props = NULL;
     *utf8dev = NULL;
+    *unique_id = NULL;
     SDL_zerop(fmt);
     if (SUCCEEDED(IMMDevice_OpenPropertyStore(device, STGM_READ, &props))) {
         PROPVARIANT var;
@@ -105,6 +108,15 @@ static void GetMMDeviceInfo(IMMDevice *device, char **utf8dev, WAVEFORMATEXTENSI
         if (SUCCEEDED(IPropertyStore_GetValue(props, &SDL_PKEY_AudioEndpoint_GUID, &var))) {
             (void)CLSIDFromString(var.pwszVal, guid);
         }
+
+        PropVariantClear(&var);
+        if (SUCCEEDED(IPropertyStore_GetValue(props, &SDL_PKEY_AudioEndpoint_StableId, &var)) && var.pwszVal) { // this was introduced in Windows 11, with stronger promises than IMMDevice_GetId().
+            *unique_id = WIN_StringToUTF8W(var.pwszVal);
+        } else if (SUCCEEDED(IMMDevice_GetId(device, &devid))) {
+            *unique_id = WIN_StringToUTF8W(devid);
+            CoTaskMemFree(devid);
+        }
+
         PropVariantClear(&var);
         IPropertyStore_Release(props);
     }
@@ -120,7 +132,7 @@ void SDL_IMMDevice_FreeDeviceHandle(SDL_AudioDevice *device)
     }
 }
 
-static SDL_AudioDevice *SDL_IMMDevice_Add(const bool recording, const char *devname, WAVEFORMATEXTENSIBLE *fmt, LPCWSTR devid, GUID *dsoundguid, SDL_AudioFormat force_format, bool supports_recording_playback_devices)
+static SDL_AudioDevice *SDL_IMMDevice_Add(const bool recording, const char *devname, WAVEFORMATEXTENSIBLE *fmt, LPCWSTR devid, GUID *dsoundguid, const char *unique_id, SDL_AudioFormat force_format, bool supports_recording_playback_devices)
 {
     /* You can have multiple endpoints on a device that are mutually exclusive ("Speakers" vs "Line Out" or whatever).
        In a perfect world, things that are unplugged won't be in this collection. The only gotcha is probably for
@@ -164,7 +176,7 @@ static SDL_AudioDevice *SDL_IMMDevice_Add(const bool recording, const char *devn
         spec.freq = fmt->Format.nSamplesPerSec;
         spec.format = (force_format != SDL_AUDIO_UNKNOWN) ? force_format : SDL_WaveFormatExToSDLFormat((WAVEFORMATEX *)fmt);
 
-        device = SDL_AddAudioDevice(recording, devname, &spec, handle);
+        device = SDL_AddAudioDevice(recording, devname, unique_id, &spec, handle);
 
         if (!recording && supports_recording_playback_devices) {
             // handle is freed by SDL_IMMDevice_FreeDeviceHandle!
@@ -181,7 +193,7 @@ static SDL_AudioDevice *SDL_IMMDevice_Add(const bool recording, const char *devn
 
             SDL_copyp(&recording_handle->directsound_guid, dsoundguid);
 
-            if (!SDL_AddAudioDevice(true, devname, &spec, recording_handle)) {
+            if (!SDL_AddAudioDevice(true, devname, unique_id, &spec, recording_handle)) {
                 SDL_free(recording_handle->immdevice_id);
                 SDL_free(recording_handle);
             }
@@ -276,13 +288,15 @@ static HRESULT STDMETHODCALLTYPE SDLMMNotificationClient_OnDeviceStateChanged(IM
                 const bool recording = (flow == eCapture);
                 if (dwNewState == DEVICE_STATE_ACTIVE) {
                     char *utf8dev;
+                    char *unique_id;
                     WAVEFORMATEXTENSIBLE fmt;
                     GUID dsoundguid;
-                    GetMMDeviceInfo(device, &utf8dev, &fmt, &dsoundguid);
+                    GetMMDeviceInfo(device, &utf8dev, &fmt, &dsoundguid, &unique_id);
                     if (utf8dev) {
-                        SDL_IMMDevice_Add(recording, utf8dev, &fmt, pwstrDeviceId, &dsoundguid, client->force_format, client->supports_recording_playback_devices);
+                        SDL_IMMDevice_Add(recording, utf8dev, &fmt, pwstrDeviceId, &dsoundguid, unique_id, client->force_format, client->supports_recording_playback_devices);
                         SDL_free(utf8dev);
                     }
+                    SDL_free(unique_id);
                 } else {
                     immcallbacks.audio_device_disconnected(SDL_IMMDevice_FindByDevID(pwstrDeviceId));
                 }
@@ -440,18 +454,20 @@ static void EnumerateEndpointsForFlow(const bool recording, SDL_AudioDevice **de
             LPWSTR devid = NULL;
             if (SUCCEEDED(IMMDevice_GetId(immdevice, &devid))) {
                 char *devname = NULL;
+                char *unique_id = NULL;
                 WAVEFORMATEXTENSIBLE fmt;
                 GUID dsoundguid;
                 SDL_zero(fmt);
                 SDL_zero(dsoundguid);
-                GetMMDeviceInfo(immdevice, &devname, &fmt, &dsoundguid);
+                GetMMDeviceInfo(immdevice, &devname, &fmt, &dsoundguid, &unique_id);
                 if (devname) {
-                    SDL_AudioDevice *sdldevice = SDL_IMMDevice_Add(recording, devname, &fmt, devid, &dsoundguid, force_format, supports_recording_playback_devices);
+                    SDL_AudioDevice *sdldevice = SDL_IMMDevice_Add(recording, devname, &fmt, devid, &dsoundguid, unique_id, force_format, supports_recording_playback_devices);
                     if (default_device && default_devid && SDL_wcscmp(default_devid, devid) == 0) {
                         *default_device = sdldevice;
                     }
                     SDL_free(devname);
                 }
+                SDL_free(unique_id);
                 CoTaskMemFree(devid);
             }
             IMMDevice_Release(immdevice);

+ 1 - 0
src/dynapi/SDL_dynapi.exports

@@ -1314,3 +1314,4 @@ _SDL_GetRenderClipRectFloat
 _SDL_GetOpenHarmonySDKVersion
 _SDL_GetOpenHarmonyInternalStoragePath
 _SDL_RequestOpenHarmonyPermission
+_SDL_GetAudioDeviceProperties

+ 1 - 0
src/dynapi/SDL_dynapi.sym

@@ -1315,6 +1315,7 @@ SDL3_0.0.0 {
     SDL_GetOpenHarmonySDKVersion;
     SDL_GetOpenHarmonyInternalStoragePath;
     SDL_RequestOpenHarmonyPermission;
+    SDL_GetAudioDeviceProperties;
     # extra symbols go here (don't modify this line)
   local: *;
 };

+ 1 - 0
src/dynapi/SDL_dynapi_overrides.h

@@ -1341,3 +1341,4 @@
 #define SDL_GetOpenHarmonySDKVersion SDL_GetOpenHarmonySDKVersion_REAL
 #define SDL_GetOpenHarmonyInternalStoragePath SDL_GetOpenHarmonyInternalStoragePath_REAL
 #define SDL_RequestOpenHarmonyPermission SDL_RequestOpenHarmonyPermission_REAL
+#define SDL_GetAudioDeviceProperties SDL_GetAudioDeviceProperties_REAL

+ 1 - 0
src/dynapi/SDL_dynapi_procs.h

@@ -1349,3 +1349,4 @@ SDL_DYNAPI_PROC(bool,SDL_GetRenderClipRectFloat,(SDL_Renderer *a,SDL_FRect *b),(
 SDL_DYNAPI_PROC(int,SDL_GetOpenHarmonySDKVersion,(void),(),return)
 SDL_DYNAPI_PROC(const char*,SDL_GetOpenHarmonyInternalStoragePath,(void),(),return)
 SDL_DYNAPI_PROC(bool,SDL_RequestOpenHarmonyPermission,(const char *a,SDL_RequestOpenHarmonyPermissionCallback b,void *c),(a,b,c),return)
+SDL_DYNAPI_PROC(SDL_PropertiesID,SDL_GetAudioDeviceProperties,(SDL_AudioDeviceID a),(a),return)

+ 9 - 4
test/testaudioinfo.c

@@ -32,11 +32,16 @@ print_devices(bool recording)
         int i;
         SDL_Log("Found %d %s device%s:", n, typestr, n != 1 ? "s" : "");
         for (i = 0; i < n; i++) {
-            const char *name = SDL_GetAudioDeviceName(devices[i]);
-            if (name) {
-                SDL_Log("  %d: %s", i, name);
+            const char *str = SDL_GetAudioDeviceName(devices[i]);
+            if (str) {
+                SDL_Log("  %d: %s", i, str);
             } else {
-                SDL_Log("  %d Error: %s", i, SDL_GetError());
+                SDL_Log("  %d SDL_GetAudioDeviceName() Error: %s", i, SDL_GetError());
+            }
+
+            str = SDL_GetStringProperty(SDL_GetAudioDeviceProperties(devices[i]), SDL_PROP_AUDIO_DEVICE_UNIQUE_ID_STRING, NULL);;
+            if (str) {
+                SDL_Log("     Unique ID: '%s'", str);
             }
 
             if (SDL_GetAudioDeviceFormat(devices[i], &spec, &frames)) {