SDL: Add SDL_GetDefaultAudioInfo.

From 2f0816adb76ac1f2a99894b5d6aeb5665f2d00eb Mon Sep 17 00:00:00 2001
From: Ethan Lee <[EMAIL REDACTED]>
Date: Mon, 11 Jul 2022 13:08:30 -0400
Subject: [PATCH] Add SDL_GetDefaultAudioInfo.

This API is supported on pipewire, pulseaudio, wasapi, and directsound.

Co-authored-by: Frank Praznik <frank.praznik@gmail.com>
---
 include/SDL_audio.h                     |  37 +++++++
 src/audio/SDL_audio.c                   |  18 ++++
 src/audio/SDL_sysaudio.h                |   1 +
 src/audio/directsound/SDL_directsound.c |  12 +++
 src/audio/pipewire/SDL_pipewire.c       | 123 ++++++++++++++++++++----
 src/audio/pulseaudio/SDL_pulseaudio.c   | 122 ++++++++++++++++++++---
 src/audio/wasapi/SDL_wasapi.c           |   1 +
 src/audio/wasapi/SDL_wasapi.h           |   1 +
 src/audio/wasapi/SDL_wasapi_win32.c     |   6 ++
 src/audio/wasapi/SDL_wasapi_winrt.cpp   |   6 ++
 src/core/windows/SDL_immdevice.c        |  34 +++++++
 src/core/windows/SDL_immdevice.h        |   1 +
 src/dynapi/SDL2.exports                 |   1 +
 src/dynapi/SDL_dynapi_overrides.h       |   1 +
 src/dynapi/SDL_dynapi_procs.h           |   1 +
 15 files changed, 334 insertions(+), 31 deletions(-)

diff --git a/include/SDL_audio.h b/include/SDL_audio.h
index 01d19787864..d759dfad28f 100644
--- a/include/SDL_audio.h
+++ b/include/SDL_audio.h
@@ -487,6 +487,7 @@ extern DECLSPEC int SDLCALL SDL_GetNumAudioDevices(int iscapture);
  * \since This function is available since SDL 2.0.0.
  *
  * \sa SDL_GetNumAudioDevices
+ * \sa SDL_GetDefaultAudioInfo
  */
 extern DECLSPEC const char *SDLCALL SDL_GetAudioDeviceName(int index,
                                                            int iscapture);
@@ -512,12 +513,48 @@ extern DECLSPEC const char *SDLCALL SDL_GetAudioDeviceName(int index,
  * \since This function is available since SDL 2.0.16.
  *
  * \sa SDL_GetNumAudioDevices
+ * \sa SDL_GetDefaultAudioInfo
  */
 extern DECLSPEC int SDLCALL SDL_GetAudioDeviceSpec(int index,
                                                    int iscapture,
                                                    SDL_AudioSpec *spec);
 
 
+/**
+ * Get the name and preferred format of the default audio device.
+ *
+ * Some (but not all!) platforms have an isolated mechanism to get information
+ * about the "default" device. This can actually be a completely different
+ * device that's not in the list you get from SDL_GetAudioDeviceSpec(). It can
+ * even be a network address! (This is discussed in SDL_OpenAudioDevice().)
+ *
+ * As a result, this call is not guaranteed to be performant, as it can query
+ * the sound server directly every time, unlike the other query functions. You
+ * should call this function sparingly!
+ *
+ * `spec` will be filled with the sample rate, sample format, and channel
+ * count, if a default device exists on the system. If `name` is provided, will
+ * be filled with either a dynamically-allocated UTF-8 string or NULL.
+ *
+ * \param name A pointer to be filled with the name of the default device (can
+               be NULL). Please call SDL_free() when you are done with this
+               pointer!
+ * \param spec The SDL_AudioSpec to be initialized by this function.
+ * \param iscapture non-zero to query the default recording device, zero to
+ *                  query the default output device.
+ * \returns 0 on success, nonzero on error
+ *
+ * \since This function is available since SDL 2.24.0.
+ *
+ * \sa SDL_GetAudioDeviceName
+ * \sa SDL_GetAudioDeviceSpec
+ * \sa SDL_OpenAudioDevice
+ */
+extern DECLSPEC int SDLCALL SDL_GetDefaultAudioInfo(char **name,
+                                                    SDL_AudioSpec *spec,
+                                                    int iscapture);
+
+
 /**
  * Open a specific audio device.
  *
diff --git a/src/audio/SDL_audio.c b/src/audio/SDL_audio.c
index 3ce3cb9d5e2..e2306bc03b1 100644
--- a/src/audio/SDL_audio.c
+++ b/src/audio/SDL_audio.c
@@ -1154,6 +1154,24 @@ SDL_GetAudioDeviceSpec(int index, int iscapture, SDL_AudioSpec *spec)
 }
 
 
+int
+SDL_GetDefaultAudioInfo(char **name, SDL_AudioSpec *spec, int iscapture)
+{
+    if (spec == NULL) {
+        return SDL_InvalidParamError("spec");
+    }
+
+    if (!SDL_GetCurrentAudioDriver()) {
+        return SDL_SetError("Audio subsystem is not initialized");
+    }
+
+    if (current_audio.impl.GetDefaultAudioInfo == NULL) {
+        return SDL_Unsupported();
+    }
+    return current_audio.impl.GetDefaultAudioInfo(name, spec, iscapture);
+}
+
+
 static void
 close_audio_device(SDL_AudioDevice * device)
 {
diff --git a/src/audio/SDL_sysaudio.h b/src/audio/SDL_sysaudio.h
index d7e1685342e..0f98f90a9c7 100644
--- a/src/audio/SDL_sysaudio.h
+++ b/src/audio/SDL_sysaudio.h
@@ -78,6 +78,7 @@ typedef struct SDL_AudioDriverImpl
     void (*UnlockDevice) (_THIS);
     void (*FreeDeviceHandle) (void *handle);  /**< SDL is done with handle from SDL_AddAudioDevice() */
     void (*Deinitialize) (void);
+    int (*GetDefaultAudioInfo) (char **name, SDL_AudioSpec *spec, int iscapture);
 
     /* !!! FIXME: add pause(), so we can optimize instead of mixing silence. */
 
diff --git a/src/audio/directsound/SDL_directsound.c b/src/audio/directsound/SDL_directsound.c
index b4b2fa2a676..19f11541567 100644
--- a/src/audio/directsound/SDL_directsound.c
+++ b/src/audio/directsound/SDL_directsound.c
@@ -156,6 +156,17 @@ DSOUND_FreeDeviceHandle(void *handle)
     SDL_free(handle);
 }
 
+static int
+DSOUND_GetDefaultAudioInfo(char **name, SDL_AudioSpec *spec, int iscapture)
+{
+#if HAVE_MMDEVICEAPI_H
+    if (SupportsIMMDevice) {
+        return SDL_IMMDevice_GetDefaultAudioInfo(name, spec, iscapture);
+    }
+#endif /* HAVE_MMDEVICEAPI_H */
+    return SDL_Unsupported();
+}
+
 static BOOL CALLBACK
 FindAllDevs(LPGUID guid, LPCWSTR desc, LPCWSTR module, LPVOID data)
 {
@@ -615,6 +626,7 @@ DSOUND_Init(SDL_AudioDriverImpl * impl)
     impl->CloseDevice = DSOUND_CloseDevice;
     impl->FreeDeviceHandle = DSOUND_FreeDeviceHandle;
     impl->Deinitialize = DSOUND_Deinitialize;
+    impl->GetDefaultAudioInfo = DSOUND_GetDefaultAudioInfo;
 
     impl->HasCaptureSupport = SDL_TRUE;
 
diff --git a/src/audio/pipewire/SDL_pipewire.c b/src/audio/pipewire/SDL_pipewire.c
index 387028edb27..3d4bec3b0e5 100644
--- a/src/audio/pipewire/SDL_pipewire.c
+++ b/src/audio/pipewire/SDL_pipewire.c
@@ -30,6 +30,7 @@
 
 #include <pipewire/extensions/metadata.h>
 #include <spa/param/audio/format-utils.h>
+#include <spa/utils/json.h>
 
 /*
  * The following keys are defined for compatability when building against older versions of Pipewire
@@ -249,7 +250,11 @@ struct io_node
     SDL_bool      is_capture;
     SDL_AudioSpec spec;
 
-    char name[];
+    /* FIXME: These sizes are arbitrary! */
+    #define MAX_FRIENDLY_NAME 256
+    #define MAX_IDENTIFIER_PATH 256
+    char name[MAX_FRIENDLY_NAME]; /* Friendly name */
+    char path[MAX_IDENTIFIER_PATH]; /* OS identifier (i.e. ALSA endpoint) */
 };
 
 /* The global hotplug thread and associated objects. */
@@ -265,8 +270,8 @@ static int                    hotplug_init_seq_val;
 static SDL_bool               hotplug_init_complete;
 static SDL_bool               hotplug_events_enabled;
 
-static Uint32 pipewire_default_sink_id   = SPA_ID_INVALID;
-static Uint32 pipewire_default_source_id = SPA_ID_INVALID;
+static char *pipewire_default_sink_id   = NULL;
+static char *pipewire_default_source_id = NULL;
 
 /* The active node list */
 static SDL_bool
@@ -324,10 +329,10 @@ io_list_sort()
 
     /* Find and move the default nodes to the beginning of the list */
     spa_list_for_each_safe (n, temp, &hotplug_io_list, link) {
-        if (n->id == pipewire_default_sink_id) {
+        if (pipewire_default_sink_id != NULL && SDL_strcmp(n->path, pipewire_default_sink_id) == 0) {
             default_sink = n;
             spa_list_remove(&n->link);
-        } else if (n->id == pipewire_default_source_id) {
+        } else if (pipewire_default_source_id != NULL && SDL_strcmp(n->path, pipewire_default_source_id) == 0) {
             default_source = n;
             spa_list_remove(&n->link);
         }
@@ -353,6 +358,18 @@ io_list_clear()
     }
 }
 
+static struct io_node*
+io_list_get(char *path)
+{
+    struct io_node *n, *temp;
+    spa_list_for_each_safe (n, temp, &hotplug_io_list, link) {
+        if (SDL_strcmp(n->path, path) == 0) {
+            return n;
+        }
+    }
+    return NULL;
+}
+
 static void
 node_object_destroy(struct node_object *node)
 {
@@ -591,17 +608,43 @@ node_event_param(void *object, int seq, uint32_t id, uint32_t index, uint32_t ne
 static const struct pw_node_events interface_node_events = { PW_VERSION_NODE_EVENTS, .info = node_event_info,
                                                              .param = node_event_param };
 
+static char*
+get_name_from_json(const char *json)
+{
+    struct spa_json parser[2];
+    char key[7]; /* "name" */
+    char value[MAX_IDENTIFIER_PATH];
+    spa_json_init(&parser[0], json, SDL_strlen(json));
+    if (spa_json_enter_object(&parser[0], &parser[1]) <= 0) {
+        /* Not actually JSON */
+        return NULL;
+    }
+    if (spa_json_get_string(&parser[1], key, sizeof(key)) <= 0) {
+        /* Not actually a key/value pair */
+        return NULL;
+    }
+    if (spa_json_get_string(&parser[1], value, sizeof(value)) <= 0) {
+        /* Somehow had a key with no value? */
+        return NULL;
+    }
+    return SDL_strdup(value);
+}
+
 /* Metadata node callback */
 static int
 metadata_property(void *object, Uint32 subject, const char *key, const char *type, const char *value)
 {
     if (subject == PW_ID_CORE && key != NULL && value != NULL) {
-        Uint32 val = SDL_atoi(value);
-
         if (!SDL_strcmp(key, "default.audio.sink")) {
-            pipewire_default_sink_id = val;
+            if (pipewire_default_sink_id != NULL) {
+                SDL_free(pipewire_default_sink_id);
+            }
+            pipewire_default_sink_id = get_name_from_json(value);
         } else if (!SDL_strcmp(key, "default.audio.source")) {
-            pipewire_default_source_id = val;
+            if (pipewire_default_source_id != NULL) {
+                SDL_free(pipewire_default_source_id);
+            }
+            pipewire_default_source_id = get_name_from_json(value);
         }
     }
 
@@ -623,9 +666,9 @@ registry_event_global_callback(void *object, uint32_t id, uint32_t permissions,
 
         if (media_class) {
             const char     *node_desc;
+            const char     *node_path;
             struct io_node *io;
             SDL_bool        is_capture;
-            int             str_buffer_len;
 
             /* Just want sink and capture */
             if (!SDL_strcasecmp(media_class, "Audio/Sink")) {
@@ -637,8 +680,9 @@ registry_event_global_callback(void *object, uint32_t id, uint32_t permissions,
             }
 
             node_desc = spa_dict_lookup(props, PW_KEY_NODE_DESCRIPTION);
+            node_path = spa_dict_lookup(props, PW_KEY_NODE_NAME);
 
-            if (node_desc) {
+            if (node_desc && node_path) {
                 node = node_object_new(id, type, version, &interface_node_events, &interface_core_events);
                 if (node == NULL) {
                     SDL_SetError("Pipewire: Failed to allocate interface node");
@@ -646,8 +690,7 @@ registry_event_global_callback(void *object, uint32_t id, uint32_t permissions,
                 }
 
                 /* Allocate and initialize the I/O node information struct */
-                str_buffer_len = SDL_strlen(node_desc) + 1;
-                node->userdata = io = SDL_calloc(1, sizeof(struct io_node) + str_buffer_len);
+                node->userdata = io = SDL_calloc(1, sizeof(struct io_node));
                 if (io == NULL) {
                     node_object_destroy(node);
                     SDL_OutOfMemory();
@@ -658,7 +701,8 @@ registry_event_global_callback(void *object, uint32_t id, uint32_t permissions,
                 io->id          = id;
                 io->is_capture  = is_capture;
                 io->spec.format = AUDIO_F32; /* Pipewire uses floats internally, other formats require conversion. */
-                SDL_strlcpy(io->name, node_desc, str_buffer_len);
+                SDL_strlcpy(io->name, node_desc, sizeof(io->name));
+                SDL_strlcpy(io->path, node_path, sizeof(io->path));
 
                 /* Update sync points */
                 hotplug_core_sync(node);
@@ -744,8 +788,14 @@ hotplug_loop_destroy()
     hotplug_init_complete  = SDL_FALSE;
     hotplug_events_enabled = SDL_FALSE;
 
-    pipewire_default_sink_id   = SPA_ID_INVALID;
-    pipewire_default_source_id = SPA_ID_INVALID;
+    if (pipewire_default_sink_id != NULL) {
+        SDL_free(pipewire_default_sink_id);
+        pipewire_default_sink_id = NULL;
+    }
+    if (pipewire_default_source_id != NULL) {
+        SDL_free(pipewire_default_source_id);
+        pipewire_default_source_id = NULL;
+    }
 
     if (hotplug_registry) {
         PIPEWIRE_pw_proxy_destroy((struct pw_proxy *)hotplug_registry);
@@ -1228,6 +1278,38 @@ static void PIPEWIRE_CloseDevice(_THIS)
     SDL_free(this->hidden);
 }
 
+static int
+PIPEWIRE_GetDefaultAudioInfo(char **name, SDL_AudioSpec *spec, int iscapture)
+{
+    struct io_node *node;
+    char *target;
+    if (iscapture) {
+        if (pipewire_default_source_id == NULL) {
+            return SDL_SetError("PipeWire could not find a default source");
+        }
+        target = pipewire_default_source_id;
+    } else {
+        if (pipewire_default_sink_id == NULL) {
+            return SDL_SetError("PipeWire could not find a default sink");
+        }
+        target = pipewire_default_sink_id;
+    }
+
+    PIPEWIRE_pw_thread_loop_lock(hotplug_loop);
+    node = io_list_get(target);
+    if (node == NULL) {
+        PIPEWIRE_pw_thread_loop_unlock(hotplug_loop);
+        return SDL_SetError("PipeWire device list is out of sync with defaults");
+    }
+
+    if (name != NULL) {
+        *name = SDL_strdup(node->name);
+    }
+    SDL_copyp(spec, &node->spec);
+    PIPEWIRE_pw_thread_loop_unlock(hotplug_loop);
+    return 0;
+}
+
 static void
 PIPEWIRE_Deinitialize()
 {
@@ -1255,10 +1337,11 @@ PIPEWIRE_Init(SDL_AudioDriverImpl *impl)
     }
 
     /* Set the function pointers */
-    impl->DetectDevices = PIPEWIRE_DetectDevices;
-    impl->OpenDevice    = PIPEWIRE_OpenDevice;
-    impl->CloseDevice   = PIPEWIRE_CloseDevice;
-    impl->Deinitialize  = PIPEWIRE_Deinitialize;
+    impl->DetectDevices       = PIPEWIRE_DetectDevices;
+    impl->OpenDevice          = PIPEWIRE_OpenDevice;
+    impl->CloseDevice         = PIPEWIRE_CloseDevice;
+    impl->Deinitialize        = PIPEWIRE_Deinitialize;
+    impl->GetDefaultAudioInfo = PIPEWIRE_GetDefaultAudioInfo;
 
     impl->HasCaptureSupport         = SDL_TRUE;
     impl->ProvidesOwnCallbackThread = SDL_TRUE;
diff --git a/src/audio/pulseaudio/SDL_pulseaudio.c b/src/audio/pulseaudio/SDL_pulseaudio.c
index 85e1386c239..b61d9989460 100644
--- a/src/audio/pulseaudio/SDL_pulseaudio.c
+++ b/src/audio/pulseaudio/SDL_pulseaudio.c
@@ -118,6 +118,7 @@ static pa_operation * (*PULSEAUDIO_pa_stream_flush) (pa_stream *,
 static int (*PULSEAUDIO_pa_stream_disconnect) (pa_stream *);
 static void (*PULSEAUDIO_pa_stream_unref) (pa_stream *);
 static void (*PULSEAUDIO_pa_stream_set_write_callback)(pa_stream *, pa_stream_request_cb_t, void *);
+static pa_operation * (*PULSEAUDIO_pa_context_get_server_info)(pa_context *, pa_server_info_cb_t, void *);
 
 static int load_pulseaudio_syms(void);
 
@@ -230,6 +231,7 @@ load_pulseaudio_syms(void)
     SDL_PULSEAUDIO_SYM(pa_channel_map_init_auto);
     SDL_PULSEAUDIO_SYM(pa_strerror);
     SDL_PULSEAUDIO_SYM(pa_stream_set_write_callback);
+    SDL_PULSEAUDIO_SYM(pa_context_get_server_info);
     return 0;
 }
 
@@ -527,7 +529,7 @@ SourceDeviceNameCallback(pa_context *c, const pa_source_info *i, int is_last, vo
 static SDL_bool
 FindDeviceName(struct SDL_PrivateAudioData *h, const SDL_bool iscapture, void *handle)
 {
-    const uint32_t idx = ((uint32_t) ((size_t) handle)) - 1;
+    const uint32_t idx = ((uint32_t) ((intptr_t) handle)) - 1;
 
     if (handle == NULL) {  /* NULL == default device. */
         return SDL_TRUE;
@@ -691,6 +693,13 @@ static pa_mainloop *hotplug_mainloop = NULL;
 static pa_context *hotplug_context = NULL;
 static SDL_Thread *hotplug_thread = NULL;
 
+/* These are the OS identifiers (i.e. ALSA strings)... */
+static char *default_sink_path = NULL;
+static char *default_source_path = NULL;
+/* ... and these are the descriptions we use in GetDefaultAudioInfo. */
+static char *default_sink_name = NULL;
+static char *default_source_name = NULL;
+
 /* device handles are device index + 1, cast to void*, so we never pass a NULL. */
 
 static SDL_AudioFormat
@@ -721,6 +730,7 @@ static void
 SinkInfoCallback(pa_context *c, const pa_sink_info *i, int is_last, void *data)
 {
     SDL_AudioSpec spec;
+    SDL_bool add = (SDL_bool) ((intptr_t) data);
     if (i) {
         spec.freq = i->sample_spec.rate;
         spec.channels = i->sample_spec.channels;
@@ -731,7 +741,16 @@ SinkInfoCallback(pa_context *c, const pa_sink_info *i, int is_last, void *data)
         spec.callback = NULL;
         spec.userdata = NULL;
 
-        SDL_AddAudioDevice(SDL_FALSE, i->description, &spec, (void *) ((size_t) i->index+1));
+        if (add) {
+            SDL_AddAudioDevice(SDL_FALSE, i->description, &spec, (void *) ((intptr_t) i->index+1));
+        }
+
+        if (default_sink_path != NULL && SDL_strcmp(i->name, default_sink_path) == 0) {
+            if (default_sink_name != NULL) {
+                SDL_free(default_sink_name);
+            }
+            default_sink_name = SDL_strdup(i->description);
+        }
     }
 }
 
@@ -740,6 +759,7 @@ static void
 SourceInfoCallback(pa_context *c, const pa_source_info *i, int is_last, void *data)
 {
     SDL_AudioSpec spec;
+    SDL_bool add = (SDL_bool) ((intptr_t) data);
     if (i) {
         /* Maybe skip "monitor" sources. These are just output from other sinks. */
         if (include_monitors || (i->monitor_of_sink == PA_INVALID_INDEX)) {
@@ -752,30 +772,59 @@ SourceInfoCallback(pa_context *c, const pa_source_info *i, int is_last, void *da
             spec.callback = NULL;
             spec.userdata = NULL;
 
-            SDL_AddAudioDevice(SDL_TRUE, i->description, &spec, (void *) ((size_t) i->index+1));
+            if (add) {
+                SDL_AddAudioDevice(SDL_TRUE, i->description, &spec, (void *) ((intptr_t) i->index+1));
+            }
+
+            if (default_source_path != NULL && SDL_strcmp(i->name, default_source_path) == 0) {
+                if (default_source_name != NULL) {
+                    SDL_free(default_source_name);
+                }
+                default_source_name = SDL_strdup(i->description);
+            }
         }
     }
 }
 
+static void
+ServerInfoCallback(pa_context *c, const pa_server_info *i, void *data)
+{
+    if (default_sink_path != NULL) {
+        SDL_free(default_sink_path);
+    }
+    if (default_source_path != NULL) {
+        SDL_free(default_source_path);
+    }
+    default_sink_path = SDL_strdup(i->default_sink_name);
+    default_source_path = SDL_strdup(i->default_source_name);
+}
+
 /* This is called when PulseAudio has a device connected/removed/changed. */
 static void
 HotplugCallback(pa_context *c, pa_subscription_event_type_t t, uint32_t idx, void *data)
 {
     const SDL_bool added = ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_NEW);
     const SDL_bool removed = ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE);
+    const SDL_bool changed = ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_CHANGE);
 
-    if (added || removed) {  /* we only care about add/remove events. */
+    if (added || removed || changed) {  /* we only care about add/remove events. */
         const SDL_bool sink = ((t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == PA_SUBSCRIPTION_EVENT_SINK);
         const SDL_bool source = ((t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == PA_SUBSCRIPTION_EVENT_SOURCE);
 
         /* adds need sink details from the PulseAudio server. Another callback... */
-        if (added && sink) {
-            PULSEAUDIO_pa_context_get_sink_info_by_index(hotplug_context, idx, SinkInfoCallback, NULL);
-        } else if (added && source) {
-            PULSEAUDIO_pa_context_get_source_info_by_index(hotplug_context, idx, SourceInfoCallback, NULL);
+        if ((added || changed) && sink) {
+            if (changed) {
+                PULSEAUDIO_pa_context_get_server_info(hotplug_context, ServerInfoCallback, NULL);
+            }
+            PULSEAUDIO_pa_context_get_sink_info_by_index(hotplug_context, idx, SinkInfoCallback, (void*) ((intptr_t) added));
+        } else if ((added || changed) && source) {
+            if (changed) {
+                PULSEAUDIO_pa_context_get_server_info(hotplug_context, ServerInfoCallback, NULL);
+            }
+            PULSEAUDIO_pa_context_get_source_info_by_index(hotplug_context, idx, SourceInfoCallback, (void*) ((intptr_t) added));
         } else if (removed && (sink || source)) {
             /* removes we can handle just with the device index. */
-            SDL_RemoveAudioDevice(source != 0, (void *) ((size_t) idx+1));
+            SDL_RemoveAudioDevice(source != 0, (void *) ((intptr_t) idx+1));
         }
     }
 }
@@ -796,13 +845,46 @@ HotplugThread(void *data)
 static void
 PULSEAUDIO_DetectDevices()
 {
-    WaitForPulseOperation(hotplug_mainloop, PULSEAUDIO_pa_context_get_sink_info_list(hotplug_context, SinkInfoCallback, NULL));
-    WaitForPulseOperation(hotplug_mainloop, PULSEAUDIO_pa_context_get_source_info_list(hotplug_context, SourceInfoCallback, NULL));
+    WaitForPulseOperation(hotplug_mainloop, PULSEAUDIO_pa_context_get_server_info(hotplug_context, ServerInfoCallback, NULL));
+    WaitForPulseOperation(hotplug_mainloop, PULSEAUDIO_pa_context_get_sink_info_list(hotplug_context, SinkInfoCallback, (void*) ((intptr_t) SDL_TRUE)));
+    WaitForPulseOperation(hotplug_mainloop, PULSEAUDIO_pa_context_get_source_info_list(hotplug_context, SourceInfoCallback, (void*) ((intptr_t) SDL_TRUE)));
 
     /* ok, we have a sane list, let's set up hotplug notifications now... */
     hotplug_thread = SDL_CreateThreadInternal(HotplugThread, "PulseHotplug", 256 * 1024, NULL);
 }
 
+static int
+PULSEAUDIO_GetDefaultAudioInfo(char **name, SDL_AudioSpec *spec, int iscapture)
+{
+    int i;
+    int numdevices;
+
+    char *target;
+    if (iscapture) {
+        if (default_source_name == NULL) {
+            return SDL_SetError("PulseAudio could not find a default source");
+        }
+        target = default_source_name;
+    } else {
+        if (default_sink_name == NULL) {
+            return SDL_SetError("PulseAudio could not find a default sink");
+        }
+        target = default_sink_name;
+    }
+
+    numdevices = SDL_GetNumAudioDevices(iscapture);
+    for (i = 0; i < numdevices; i += 1) {
+        if (SDL_strcmp(SDL_GetAudioDeviceName(i, iscapture), target) == 0) {
+            if (name != NULL) {
+                *name = SDL_strdup(target);
+            }
+            SDL_GetAudioDeviceSpec(i, iscapture, spec);
+            return 0;
+        }
+    }
+    return SDL_SetError("Could not find default PulseAudio device");
+}
+
 static void
 PULSEAUDIO_Deinitialize(void)
 {
@@ -816,6 +898,23 @@ PULSEAUDIO_Deinitialize(void)
     hotplug_mainloop = NULL;
     hotplug_context = NULL;
 
+    if (default_sink_path != NULL) {
+        SDL_free(default_sink_path);
+        default_sink_path = NULL;
+    }
+    if (default_source_path != NULL) {
+        SDL_free(default_source_path);
+        default_source_path = NULL;
+    }
+    if (default_sink_name != NULL) {
+        SDL_free(default_sink_name);
+        default_sink_name = NULL;
+    }
+    if (default_source_name != NULL) {
+        SDL_free(default_source_name);
+        default_source_name = NULL;
+    }
+
     UnloadPulseAudioLibrary();
 }
 
@@ -843,6 +942,7 @@ PULSEAUDIO_Init(SDL_AudioDriverImpl * impl)
     impl->Deinitialize = PULSEAUDIO_Deinitialize;
     impl->CaptureFromDevice = PULSEAUDIO_CaptureFromDevice;
     impl->FlushCapture = PULSEAUDIO_FlushCapture;
+    impl->GetDefaultAudioInfo = PULSEAUDIO_GetDefaultAudioInfo;
 
     impl->HasCaptureSupport = SDL_TRUE;
 
diff --git a/src/audio/wasapi/SDL_wasapi.c b/src/audio/wasapi/SDL_wasapi.c
index a678ca3e52b..5b6b8216483 100644
--- a/src/audio/wasapi/SDL_wasapi.c
+++ b/src/audio/wasapi/SDL_wasapi.c
@@ -634,6 +634,7 @@ WASAPI_Init(SDL_AudioDriverImpl * impl)
     impl->FlushCapture = WASAPI_FlushCapture;
     impl->CloseDevice = WASAPI_CloseDevice;
     impl->Deinitialize = WASAPI_Deinitialize;
+    impl->GetDefaultAudioInfo = WASAPI_GetDefaultAudioInfo;
     impl->HasCaptureSupport = SDL_TRUE;
 
     return SDL_TRUE;   /* this audio target is available. */
diff --git a/src/audio/wasapi/SDL_wasapi.h b/src/audio/wasapi/SDL_wasapi.h
index d4e81a3517e..69060677d25 100644
--- a/src/audio/wasapi/SDL_wasapi.h
+++ b/src/audio/wasapi/SDL_wasapi.h
@@ -64,6 +64,7 @@ void WASAPI_UnrefDevice(_THIS);
 int WASAPI_PlatformInit(void);
 void WASAPI_PlatformDeinit(void);
 void WASAPI_EnumerateEndpoints(void);
+int WASAPI_GetDefaultAudioInfo(char **name, SDL_AudioSpec *spec, int iscapture);
 int WASAPI_ActivateDevice(_THIS, const SDL_bool isrecovery);
 void WASAPI_PlatformThreadInit(_THIS);
 void WASAPI_PlatformThreadDeinit(_THIS);
diff --git a/src/audio/wasapi/SDL_wasapi_win32.c b/src/audio/wasapi/SDL_wasapi_win32.c
index ba8962f2929..d76c8037486 100644
--- a/src/audio/wasapi/SDL_wasapi_win32.c
+++ b/src/audio/wasapi/SDL_wasapi_win32.c
@@ -145,6 +145,12 @@ WASAPI_EnumerateEndpoints(void)
     SDL_IMMDevice_EnumerateEndpoints(SDL_FALSE);
 }
 
+int
+WASAPI_GetDefaultAudioInfo(char **name, SDL_AudioSpec *spec, int iscapture)
+{
+    return SDL_IMMDevice_GetDefaultAudioInfo(name, spec, iscapture);
+}
+
 void
 WASAPI_PlatformDeleteActivationHandler(void *handler)
 {
diff --git a/src/audio/wasapi/SDL_wasapi_winrt.cpp b/src/audio/wasapi/SDL_wasapi_winrt.cpp
index 11ea0de064f..acaaf0be53e 100644
--- a/src/audio/wasapi/SDL_wasapi_winrt.cpp
+++ b/src/audio/wasapi/SDL_wasapi_winrt.cpp
@@ -243,6 +243,12 @@ WASAPI_PlatformDeleteActivationHandler(void *handler)
     ((SDL_WasapiActivationHandler *) handler)->Release();
 }
 
+int
+WASAPI_GetDefaultAudioInfo(char **name, SDL_AudioSpec *spec, int iscapture)
+{
+    return SDL_Unsupported();
+}
+
 int
 WASAPI_ActivateDevice(_THIS, const SDL_bool isrecovery)
 {
diff --git a/src/core/windows/SDL_immdevice.c b/src/core/windows/SDL_immdevice.c
index 3b5714c23c7..d626ddf79d3 100644
--- a/src/core/windows/SDL_immdevice.c
+++ b/src/core/windows/SDL_immdevice.c
@@ -471,6 +471,40 @@ SDL_IMMDevice_EnumerateEndpoints(SDL_bool useguid)
     IMMDeviceEnumerator_RegisterEndpointNotificationCallback(enumerator, (IMMNotificationClient *)&notification_client);
 }
 
+int
+SDL_IMMDevice_GetDefaultAudioInfo(char **name, SDL_AudioSpec *spec, int iscapture)
+{
+    WAVEFORMATEXTENSIBLE fmt;
+    IMMDevice *device = NULL;
+    char *filler;
+    GUID morefiller;
+    const EDataFlow dataflow = iscapture ? eCapture : eRender;
+    HRESULT ret = IMMDeviceEnumerator_GetDefaultAudioEndpoint(enumerator, dataflow, SDL_IMMDevice_role, &device);
+
+    if (FAILED(ret)) {
+        SDL_assert(device == NULL);
+        return WIN_SetErrorFromHRESULT("WASAPI can't find default audio endpoint", ret);
+    }
+
+    if (name == NULL) {
+        name = &filler;
+    }
+
+    SDL_zero(fmt);
+    GetMMDeviceInfo(device, name, &fmt, &morefiller);
+    IMMDevice_Release(device);
+
+    if (name == &filler) {
+        SDL_free(filler);
+    }
+
+    SDL_zerop(spec);
+    spec->channels = (Uint8)fmt.Format.nChannels;
+    spec->freq = fmt.Format.nSamplesPerSec;
+    spec->format = WaveFormatToSDLFormat((WAVEFORMATEX *) &fmt);
+    return 0;
+}
+
 SDL_AudioFormat
 WaveFormatToSDLFormat(WAVEFORMATEX *waveformat)
 {
diff --git a/src/core/windows/SDL_immdevice.h b/src/core/windows/SDL_immdevice.h
index cdb87c5fbb5..007775f9e82 100644
--- a/src/core/windows/SDL_immdevice.h
+++ b/src/core/windows/SDL_immdevice.h
@@ -33,6 +33,7 @@ int SDL_IMMDevice_Init(void);
 void SDL_IMMDevice_Quit(void);
 int SDL_IMMDevice_Get(LPCWSTR devid, IMMDevice **device, SDL_bool iscapture);
 void SDL_IMMDevice_EnumerateEndpoints(SDL_bool useguid);
+int SDL_IMMDevice_GetDefaultAudioInfo(char **name, SDL_AudioSpec *spec, int iscapture);
 
 SDL_AudioFormat WaveFormatToSDLFormat(WAVEFORMATEX *waveformat);
 
diff --git a/src/dynapi/SDL2.exports b/src/dynapi/SDL2.exports
index 082de10011f..e5aed22d049 100644
--- a/src/dynapi/SDL2.exports
+++ b/src/dynapi/SDL2.exports
@@ -853,3 +853,4 @@
 # ++'_SDL_GDKRunApp'.'SDL2.dll'.'SDL_GDKRunApp'
 ++'_SDL_GetOriginalMemoryFunctions'.'SDL2.dll'.'SDL_GetOriginalMemoryFunctions'
 ++'_SDL_ResetKeyboard'.'SDL2.dll'.'SDL_ResetKeyboard'
+++'_SDL_GetDefaultAudioInfo'.'SDL2.dll'.'SDL_GetDefaultAudioInfo'
diff --git a/src/dynapi/SDL_dynapi_overrides.h b/src/dynapi/SDL_dynapi_overrides.h
index 18e406e5f5f..8e26c34ac76 100644
--- a/src/dynapi/SDL_dynapi_overrides.h
+++ b/src/dynapi/SDL_dynapi_overrides.h
@@ -879,3 +879,4 @@
 #define SDL_GDKRunApp SDL_GDKRunApp_REAL
 #define SDL_GetOriginalMemoryFunctions SDL_GetOriginalMemoryFunctions_REAL
 #define SDL_ResetKeyboard SDL_ResetKeyboard_REAL
+#define SDL_GetDefaultAudioInfo SDL_GetDefaultAudioInfo_REAL
diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h
index a76806acce8..6b4c5c7dd9a 100644
--- a/src/dynapi/SDL_dynapi_procs.h
+++ b/src/dynapi/SDL_dynapi_procs.h
@@ -962,3 +962,4 @@ SDL_DYNAPI_PROC(int,SDL_GDKRunApp,(SDL_main_func a, void *b),(a,b),return)
 #endif
 SDL_DYNAPI_PROC(void,SDL_GetOriginalMemoryFunctions,(SDL_malloc_func *a, SDL_calloc_func *b, SDL_realloc_func *c, SDL_free_func *d),(a,b,c,d),)
 SDL_DYNAPI_PROC(void,SDL_ResetKeyboard,(void),(),)
+SDL_DYNAPI_PROC(int,SDL_GetDefaultAudioInfo,(char **a, SDL_AudioSpec *b, int c),(a,b,c),return)