# SDL: audio: Added SDL\_GetAudioDeviceProperties() and a unique-id property for it.

**URL:** <https://discourse.libsdl.org/t/sdl-audio-added-sdl-getaudiodeviceproperties-and-a-unique-id-property-for-it/69360>\
**Category:** SDL Commits\
**Created:** [September 21, 2026, 2:23pm UTC](https://discourse.libsdl.org/t/sdl-audio-added-sdl-getaudiodeviceproperties-and-a-unique-id-property-for-it/69360 "2026-09-21T14:23:10Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

**Author:** ![SDLPushPostBot](https://discourse.libsdl.org/user_avatar/discourse.libsdl.org/sdlpushpostbot/32/4370_2.png) [@SDLPushPostBot](https://discourse.libsdl.org/u/SDLPushPostBot)\
**Post date:** [September 21, 2026, 2:23pm UTC](https://discourse.libsdl.org/t/sdl-audio-added-sdl-getaudiodeviceproperties-and-a-unique-id-property-for-it/69360/1 "2026-09-21T14:23:10Z")

</div>

> <https://github.com/libsdl-org/SDL/commit/0ccb0bf6269aa9752784c55ab54660936890e301>
>
> In addition to adding properties to audio devices, the new property,
> \`SDL\_PROP\_A…UDIO\_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.

```diff
From 0ccb0bf6269aa9752784c55ab54660936890e301 Mon Sep 17 00:00:00 2001
From: "Ryan C. Gordon" <[EMAIL REDACTED]>
Date: Tue, 15 Sep 2026 18:18:56 -0400
Subject: [PATCH] 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.
---
 include/SDL3/SDL_audio.h | 45 +++++++++++
 src/audio/SDL_audio.c | 68 +++++++++++++---
 src/audio/SDL_audiodev.c | 2 +-
 src/audio/SDL_sysaudio.h | 13 ++-
 src/audio/alsa/SDL_alsa_audio.c | 6 +-
 src/audio/coreaudio/SDL_coreaudio.m | 100 ++++++++++++++----------
 src/audio/directsound/SDL_directsound.c | 9 +--
 src/audio/disk/SDL_diskaudio.c | 4 +-
 src/audio/pipewire/SDL_pipewire.c | 4 +-
 src/audio/pulseaudio/SDL_pulseaudio.c | 2 +-
 src/audio/qnx/SDL_qsa_audio.c | 2 +-
 src/audio/sndio/SDL_sndioaudio.c | 5 +-
 src/core/android/SDL_android.c | 2 +-
 src/core/windows/SDL_immdevice.c | 32 ++++++--
 src/dynapi/SDL_dynapi.exports | 1 +
 src/dynapi/SDL_dynapi.sym | 1 +
 src/dynapi/SDL_dynapi_overrides.h | 1 +
 src/dynapi/SDL_dynapi_procs.h | 1 +
 test/testaudioinfo.c | 13 ++-
 19 files changed, 228 insertions(+), 83 deletions(-)

diff --git a/include/SDL3/SDL_audio.h b/include/SDL3/SDL_audio.h
index ad066572994c1..2b78f54cc97c4 100644
--- a/include/SDL3/SDL_audio.h
+++ b/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.
  *
diff --git a/src/audio/SDL_audio.c b/src/audio/SDL_audio.c
index 7cebc542c104f..140493ed1bcb5 100644
--- a/src/audio/SDL_audio.c
+++ b/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
diff --git a/src/audio/SDL_audiodev.c b/src/audio/SDL_audiodev.c
index 57bb6827e9105..4a1a1cf21b8ed 100644
--- a/src/audio/SDL_audiodev.c
+++ b/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);
diff --git a/src/audio/SDL_sysaudio.h b/src/audio/SDL_sysaudio.h
index 76b8443f28d58..5999f846bae9e 100644
--- a/src/audio/SDL_sysaudio.h
+++ b/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;
 
diff --git a/src/audio/alsa/SDL_alsa_audio.c b/src/audio/alsa/SDL_alsa_audio.c
index 976bfbd6d00b1..f229bc085f7a3 100644
--- a/src/audio/alsa/SDL_alsa_audio.c
+++ b/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()) {
diff --git a/src/audio/coreaudio/SDL_coreaudio.m b/src/audio/coreaudio/SDL_coreaudio.m
index 5fb3cb74cb508..ecc87a1fc2dc6 100644
--- a/src/audio/coreaudio/SDL_coreaudio.m
+++ b/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.
         }
     }
 
diff --git a/src/audio/directsound/SDL_directsound.c b/src/audio/directsound/SDL_directsound.c
index 89480ee49d5f6..9e13574713c16 100644
--- a/src/audio/directsound/SDL_directsound.c
+++ b/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;
diff --git a/src/audio/disk/SDL_diskaudio.c b/src/audio/disk/SDL_diskaudio.c
index d6c214e91b447..6a58ac860d10c 100644
--- a/src/audio/disk/SDL_diskaudio.c
+++ b/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)
diff --git a/src/audio/pipewire/SDL_pipewire.c b/src/audio/pipewire/SDL_pipewire.c
index 35813c78f7312..15095a25e1d9e 100644
--- a/src/audio/pipewire/SDL_pipewire.c
+++ b/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;
diff --git a/src/audio/pulseaudio/SDL_pulseaudio.c b/src/audio/pulseaudio/SDL_pulseaudio.c
index 2fcf078fe1d3e..7e8dc26be2bab 100644
--- a/src/audio/pulseaudio/SDL_pulseaudio.c
+++ b/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);
         }
     }
 }
diff --git a/src/audio/qnx/SDL_qsa_audio.c b/src/audio/qnx/SDL_qsa_audio.c
index be1cfa3141577..a43e44dc56075 100644
--- a/src/audio/qnx/SDL_qsa_audio.c
+++ b/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
diff --git a/src/audio/sndio/SDL_sndioaudio.c b/src/audio/sndio/SDL_sndioaudio.c
index 4daab722ba1fa..81545bfcb2273 100644
--- a/src/audio/sndio/SDL_sndioaudio.c
+++ b/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)
diff --git a/src/core/android/SDL_android.c b/src/core/android/SDL_android.c
index f9515a4ca8d60..c213a0704a00e 100644
--- a/src/core/android/SDL_android.c
+++ b/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);
         }
     }
diff --git a/src/core/windows/SDL_immdevice.c b/src/core/windows/SDL_immdevice.c
index 64cea6d2c97c3..27f66e369c740 100644
--- a/src/core/windows/SDL_immdevice.c
+++ b/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_REA

```

(Patch may be truncated, please check the link at the top of this post.)
