SDL: Initial port to SDL3 audio subsystem

From 641deb9c0eb3783426223316096a2f6c32a56c9f Mon Sep 17 00:00:00 2001
From: "Joshua T. Fisher" <[EMAIL REDACTED]>
Date: Sun, 2 Nov 2025 21:29:58 -0800
Subject: [PATCH] Initial port to SDL3 audio subsystem

---
 src/audio/directsound/SDL_directsound.c |  2 +-
 src/audio/wasapi/SDL_wasapi.c           | 11 +++++-
 src/audio/wasapi/SDL_wasapi.h           |  1 +
 src/core/windows/SDL_immdevice.c        | 50 +++++++++++++++++++++----
 src/core/windows/SDL_immdevice.h        |  3 +-
 5 files changed, 56 insertions(+), 11 deletions(-)

diff --git a/src/audio/directsound/SDL_directsound.c b/src/audio/directsound/SDL_directsound.c
index c7d5db525fc4e..2cc24fa9909a6 100644
--- a/src/audio/directsound/SDL_directsound.c
+++ b/src/audio/directsound/SDL_directsound.c
@@ -206,7 +206,7 @@ static void DSOUND_DetectDevices(SDL_AudioDevice **default_playback, SDL_AudioDe
 {
 #ifdef HAVE_MMDEVICEAPI_H
     if (SupportsIMMDevice) {
-        SDL_IMMDevice_EnumerateEndpoints(default_playback, default_recording, SDL_AUDIO_UNKNOWN);
+        SDL_IMMDevice_EnumerateEndpoints(default_playback, default_recording, SDL_AUDIO_UNKNOWN, false);
     } else
 #endif
     {
diff --git a/src/audio/wasapi/SDL_wasapi.c b/src/audio/wasapi/SDL_wasapi.c
index 57a1d2c24a925..8a9907eb65518 100644
--- a/src/audio/wasapi/SDL_wasapi.c
+++ b/src/audio/wasapi/SDL_wasapi.c
@@ -62,6 +62,7 @@ static const IID SDL_IID_IAudioClient3 = { 0x7ed4ee07, 0x8e67, 0x4cd4, { 0x8c, 0
 #endif //
 
 static bool immdevice_initialized = false;
+static bool supports_recording_on_playback_devices = false;
 
 // WASAPI is _really_ particular about various things happening on the same thread, for COM and such,
 //  so we proxy various stuff to a single background thread to manage.
@@ -329,7 +330,7 @@ typedef struct
 static bool mgmtthrtask_DetectDevices(void *userdata)
 {
     mgmtthrtask_DetectDevicesData *data = (mgmtthrtask_DetectDevicesData *)userdata;
-    SDL_IMMDevice_EnumerateEndpoints(data->default_playback, data->default_recording, SDL_AUDIO_F32);
+    SDL_IMMDevice_EnumerateEndpoints(data->default_playback, data->default_recording, SDL_AUDIO_F32, supports_recording_on_playback_devices);
     return true;
 }
 
@@ -446,6 +447,8 @@ static bool mgmtthrtask_ActivateDevice(void *userdata)
         return false; // This is already set by SDL_IMMDevice_Get
     }
 
+    device->hidden->isplayback = !SDL_IMMDevice_GetIsCapture(immdevice);
+
     // this is _not_ async in standard win32, yay!
     HRESULT ret = IMMDevice_Activate(immdevice, &SDL_IID_IAudioClient, CLSCTX_ALL, NULL, (void **)&device->hidden->client);
     IMMDevice_Release(immdevice);
@@ -725,6 +728,11 @@ static bool mgmtthrtask_PrepDevice(void *userdata)
 
     newspec.freq = waveformat->nSamplesPerSec;
 
+    if (device->recording && device->hidden->isplayback)
+    {
+        streamflags |= AUDCLNT_STREAMFLAGS_LOOPBACK;
+    }
+
     streamflags |= AUDCLNT_STREAMFLAGS_EVENTCALLBACK;
 
     int new_sample_frames = 0;
@@ -978,6 +986,7 @@ static bool WASAPI_Init(SDL_AudioDriverImpl *impl)
     impl->FreeDeviceHandle = WASAPI_FreeDeviceHandle;
 
     impl->HasRecordingSupport = true;
+    supports_recording_on_playback_devices = SDL_GetHintBoolean(SDL_HINT_AUDIO_INCLUDE_MONITORS, false);
 
     return true;
 }
diff --git a/src/audio/wasapi/SDL_wasapi.h b/src/audio/wasapi/SDL_wasapi.h
index 5e528dc7850e4..3c06fdf059e73 100644
--- a/src/audio/wasapi/SDL_wasapi.h
+++ b/src/audio/wasapi/SDL_wasapi.h
@@ -43,6 +43,7 @@ struct SDL_PrivateAudioData
     SDL_AtomicInt device_disconnecting;
     bool device_lost;
     bool device_dead;
+    bool isplayback;
 };
 
 // win32 implementation calls into these.
diff --git a/src/core/windows/SDL_immdevice.c b/src/core/windows/SDL_immdevice.c
index cc6945b1bcb22..e6cf1cec198d8 100644
--- a/src/core/windows/SDL_immdevice.c
+++ b/src/core/windows/SDL_immdevice.c
@@ -120,7 +120,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)
+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)
 {
     /* 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
@@ -165,6 +165,22 @@ static SDL_AudioDevice *SDL_IMMDevice_Add(const bool recording, const char *devn
         spec.format = (force_format != SDL_AUDIO_UNKNOWN) ? force_format : SDL_WaveFormatExToSDLFormat((WAVEFORMATEX *)fmt);
 
         device = SDL_AddAudioDevice(recording, devname, &spec, handle);
+
+        if (!recording && supports_recording_playback_devices) {
+            // handle is freed by SDL_IMMDevice_FreeDeviceHandle!
+            SDL_IMMDevice_HandleData *recording_handle = (SDL_IMMDevice_HandleData *)SDL_malloc(sizeof(SDL_IMMDevice_HandleData));
+            if (!recording_handle) {
+                return NULL;
+            }
+
+            SDL_memcpy(&recording_handle->directsound_guid, dsoundguid, sizeof(GUID));
+            recording_handle->immdevice_id = SDL_wcsdup(devid);
+
+            if (!recording_handle->immdevice_id || !SDL_AddAudioDevice(true, devname, &spec, recording_handle)) {
+                SDL_free(recording_handle);
+            }
+        }
+
         if (!device) {
             SDL_free(handle->immdevice_id);
             SDL_free(handle);
@@ -184,6 +200,7 @@ typedef struct SDLMMNotificationClient
     const IMMNotificationClientVtbl *lpVtbl;
     SDL_AtomicInt refcount;
     SDL_AudioFormat force_format;
+    bool supports_recording_playback_devices;
 } SDLMMNotificationClient;
 
 static HRESULT STDMETHODCALLTYPE SDLMMNotificationClient_QueryInterface(IMMNotificationClient *client, REFIID iid, void **ppv)
@@ -257,7 +274,7 @@ static HRESULT STDMETHODCALLTYPE SDLMMNotificationClient_OnDeviceStateChanged(IM
                     GUID dsoundguid;
                     GetMMDeviceInfo(device, &utf8dev, &fmt, &dsoundguid);
                     if (utf8dev) {
-                        SDL_IMMDevice_Add(recording, utf8dev, &fmt, pwstrDeviceId, &dsoundguid, client->force_format);
+                        SDL_IMMDevice_Add(recording, utf8dev, &fmt, pwstrDeviceId, &dsoundguid, client->force_format, client->supports_recording_playback_devices);
                         SDL_free(utf8dev);
                     }
                 } else {
@@ -288,7 +305,7 @@ static const IMMNotificationClientVtbl notification_client_vtbl = {
     SDLMMNotificationClient_OnPropertyValueChanged
 };
 
-static SDLMMNotificationClient notification_client = { &notification_client_vtbl, { 1 }, SDL_AUDIO_UNKNOWN };
+static SDLMMNotificationClient notification_client = { &notification_client_vtbl, { 1 }, SDL_AUDIO_UNKNOWN, false };
 
 bool SDL_IMMDevice_Init(const SDL_IMMDevice_callbacks *callbacks)
 {
@@ -365,7 +382,23 @@ bool SDL_IMMDevice_Get(SDL_AudioDevice *device, IMMDevice **immdevice, bool reco
     return true;
 }
 
-static void EnumerateEndpointsForFlow(const bool recording, SDL_AudioDevice **default_device, SDL_AudioFormat force_format)
+bool SDL_IMMDevice_GetIsCapture(IMMDevice *device)
+{
+    bool iscapture = false;
+    IMMEndpoint *endpoint = NULL;
+    if (SUCCEEDED(IMMDevice_QueryInterface(device, &SDL_IID_IMMEndpoint, (void **)&endpoint))) {
+        EDataFlow flow;
+
+        if (SUCCEEDED(IMMEndpoint_GetDataFlow(endpoint, &flow))) {
+            iscapture = (flow == eCapture);
+        }
+    }
+
+    IMMEndpoint_Release(endpoint);
+    return iscapture;
+}
+
+static void EnumerateEndpointsForFlow(const bool recording, SDL_AudioDevice **default_device, SDL_AudioFormat force_format, bool supports_recording_playback_devices)
 {
     /* Note that WASAPI separates "adapter devices" from "audio endpoint devices"
        ...one adapter device ("SoundBlaster Pro") might have multiple endpoint devices ("Speakers", "Line-Out"). */
@@ -407,7 +440,7 @@ static void EnumerateEndpointsForFlow(const bool recording, SDL_AudioDevice **de
                 SDL_zero(dsoundguid);
                 GetMMDeviceInfo(immdevice, &devname, &fmt, &dsoundguid);
                 if (devname) {
-                    SDL_AudioDevice *sdldevice = SDL_IMMDevice_Add(recording, devname, &fmt, devid, &dsoundguid, force_format);
+                    SDL_AudioDevice *sdldevice = SDL_IMMDevice_Add(recording, devname, &fmt, devid, &dsoundguid, force_format, supports_recording_playback_devices);
                     if (default_device && default_devid && SDL_wcscmp(default_devid, devid) == 0) {
                         *default_device = sdldevice;
                     }
@@ -424,12 +457,13 @@ static void EnumerateEndpointsForFlow(const bool recording, SDL_AudioDevice **de
     IMMDeviceCollection_Release(collection);
 }
 
-void SDL_IMMDevice_EnumerateEndpoints(SDL_AudioDevice **default_playback, SDL_AudioDevice **default_recording, SDL_AudioFormat force_format)
+void SDL_IMMDevice_EnumerateEndpoints(SDL_AudioDevice **default_playback, SDL_AudioDevice **default_recording, SDL_AudioFormat force_format, bool supports_recording_playback_devices)
 {
-    EnumerateEndpointsForFlow(false, default_playback, force_format);
-    EnumerateEndpointsForFlow(true, default_recording, force_format);
+    EnumerateEndpointsForFlow(false, default_playback, force_format, supports_recording_playback_devices);
+    EnumerateEndpointsForFlow(true, default_recording, force_format, supports_recording_playback_devices);
 
     notification_client.force_format = force_format;
+    notification_client.supports_recording_playback_devices = supports_recording_playback_devices;
 
     // if this fails, we just won't get hotplug events. Carry on anyhow.
     IMMDeviceEnumerator_RegisterEndpointNotificationCallback(enumerator, (IMMNotificationClient *)&notification_client);
diff --git a/src/core/windows/SDL_immdevice.h b/src/core/windows/SDL_immdevice.h
index 0582bc0dedae6..4e84e13104852 100644
--- a/src/core/windows/SDL_immdevice.h
+++ b/src/core/windows/SDL_immdevice.h
@@ -37,7 +37,8 @@ typedef struct SDL_IMMDevice_callbacks
 bool SDL_IMMDevice_Init(const SDL_IMMDevice_callbacks *callbacks);
 void SDL_IMMDevice_Quit(void);
 bool SDL_IMMDevice_Get(struct SDL_AudioDevice *device, IMMDevice **immdevice, bool recording);
-void SDL_IMMDevice_EnumerateEndpoints(struct SDL_AudioDevice **default_playback, struct SDL_AudioDevice **default_recording, SDL_AudioFormat force_format);
+bool SDL_IMMDevice_GetIsCapture(IMMDevice* device);
+void SDL_IMMDevice_EnumerateEndpoints(struct SDL_AudioDevice **default_playback, struct SDL_AudioDevice **default_recording, SDL_AudioFormat force_format, bool supports_recording_playback_devices);
 LPGUID SDL_IMMDevice_GetDirectSoundGUID(struct SDL_AudioDevice *device);
 LPCWSTR SDL_IMMDevice_GetDevID(struct SDL_AudioDevice *device);
 void SDL_IMMDevice_FreeDeviceHandle(struct SDL_AudioDevice *device);