SDL: Added WASAPI support for SDL_HINT_AUDIO_DEVICE_STREAM_ROLE

From a58ae3a94f9e004c531170ff167782828c659d39 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Mon, 13 Oct 2025 11:53:10 -0700
Subject: [PATCH] Added WASAPI support for SDL_HINT_AUDIO_DEVICE_STREAM_ROLE

Also added SDL_HINT_AUDIO_DEVICE_RAW_STREAM

Fixes https://github.com/libsdl-org/SDL/issues/14091
---
 include/SDL3/SDL_hints.h      | 29 +++++++++++++++++++++++++
 src/audio/wasapi/SDL_wasapi.c | 41 +++++++++++++++++++++++++++++++++++
 test/loopwave.c               |  8 +++++--
 3 files changed, 76 insertions(+), 2 deletions(-)

diff --git a/include/SDL3/SDL_hints.h b/include/SDL3/SDL_hints.h
index 7b59a71df07c6..60228f78760d3 100644
--- a/include/SDL3/SDL_hints.h
+++ b/include/SDL3/SDL_hints.h
@@ -391,12 +391,41 @@ extern "C" {
  * concept, so it applies to a physical audio device in this case, and not an
  * SDL_AudioStream, nor an SDL logical audio device.
  *
+ * For Windows WASAPI audio, the following roles are supported, and map to `AUDIO_STREAM_CATEGORY`:
+ *
+ * - "Other" (default)
+ * - "Communications" - Real-time communications, such as VOIP or chat
+ * - "Game" - Game audio
+ * - "GameChat" - Game chat audio, similar to "Communications" except that this will not attenuate other audio streams
+ * - "Movie" - Music or sound with dialog
+ * - "Media" - Music or sound without dialog
+ *
+ * If your application applies its own echo cancellation, gain control, and noise reduction it should also set SDL_HINT_AUDIO_DEVICE_RAW_STREAM.
+ *
  * This hint should be set before an audio device is opened.
  *
  * \since This hint is available since SDL 3.2.0.
  */
 #define SDL_HINT_AUDIO_DEVICE_STREAM_ROLE "SDL_AUDIO_DEVICE_STREAM_ROLE"
 
+/**
+ * Specify whether this audio device should do audio processing.
+ *
+ * Some operating systems perform echo cancellation, gain control, and noise reduction as needed. If your application already handles these, you can set this hint to prevent the OS from doing additional audio processing.
+ *
+ * This corresponds to the WASAPI audio option `AUDCLNT_STREAMOPTIONS_RAW`.
+ *
+ * The variable can be set to the following values:
+ *
+ * - "0": audio processing can be done by the OS. (default)
+ * - "1": audio processing is done by the application.
+ *
+ * This hint should be set before an audio device is opened.
+ *
+ * \since This hint is available since SDL 3.4.0.
+ */
+#define SDL_HINT_AUDIO_DEVICE_RAW_STREAM "SDL_AUDIO_DEVICE_RAW_STREAM"
+
 /**
  * Specify the input file when recording audio using the disk audio driver.
  *
diff --git a/src/audio/wasapi/SDL_wasapi.c b/src/audio/wasapi/SDL_wasapi.c
index 0a06f2c6d68c5..443bbd536773e 100644
--- a/src/audio/wasapi/SDL_wasapi.c
+++ b/src/audio/wasapi/SDL_wasapi.c
@@ -54,6 +54,9 @@ static pfnAvRevertMmThreadCharacteristics pAvRevertMmThreadCharacteristics = NUL
 static const IID SDL_IID_IAudioRenderClient = { 0xf294acfc, 0x3146, 0x4483, { 0xa7, 0xbf, 0xad, 0xdc, 0xa7, 0xc2, 0x60, 0xe2 } };
 static const IID SDL_IID_IAudioCaptureClient = { 0xc8adbd64, 0xe71e, 0x48a0, { 0xa4, 0xde, 0x18, 0x5c, 0x39, 0x5c, 0xd3, 0x17 } };
 static const IID SDL_IID_IAudioClient = { 0x1cb9ad4c, 0xdbfa, 0x4c32, { 0xb1, 0x78, 0xc2, 0xf5, 0x68, 0xa7, 0x03, 0xb2 } };
+#ifdef __IAudioClient2_INTERFACE_DEFINED__
+static const IID SDL_IID_IAudioClient2 = { 0x726778cd, 0xf60a, 0x4EDA, { 0x82, 0xde, 0xe4, 0x76, 0x10, 0xcd, 0x78, 0xaa } };
+#endif //
 #ifdef __IAudioClient3_INTERFACE_DEFINED__
 static const IID SDL_IID_IAudioClient3 = { 0x7ed4ee07, 0x8e67, 0x4cd4, { 0x8c, 0x1a, 0x2b, 0x7a, 0x59, 0x87, 0xad, 0x42 } };
 #endif //
@@ -727,6 +730,44 @@ static bool mgmtthrtask_PrepDevice(void *userdata)
     int new_sample_frames = 0;
     bool iaudioclient3_initialized = false;
 
+#ifdef __IAudioClient2_INTERFACE_DEFINED__
+    IAudioClient2 *client2 = NULL;
+    ret = IAudioClient_QueryInterface(client, &SDL_IID_IAudioClient2, (void **)&client2);
+    if (SUCCEEDED(ret)) {
+        AudioClientProperties audioProps;
+
+        SDL_zero(audioProps);
+        audioProps.cbSize = sizeof(audioProps);
+
+        const char *hint = SDL_GetHint(SDL_HINT_AUDIO_DEVICE_STREAM_ROLE);
+        if (hint && *hint) {
+            if (SDL_strcasecmp(hint, "Communications") == 0) {
+                audioProps.eCategory = AudioCategory_Communications;
+            } else if (SDL_strcasecmp(hint, "Game") == 0) {
+                // We'll add support for GameEffects as distinct from GameMedia later when we add stream roles
+                audioProps.eCategory = AudioCategory_GameEffects;
+            } else if (SDL_strcasecmp(hint, "GameChat") == 0) {
+                audioProps.eCategory = AudioCategory_GameChat;
+            } else if (SDL_strcasecmp(hint, "Movie") == 0) {
+                audioProps.eCategory = AudioCategory_Movie;
+            } else if (SDL_strcasecmp(hint, "Media") == 0) {
+                audioProps.eCategory = AudioCategory_Media;
+            }
+        }
+
+        if (SDL_GetHintBoolean(SDL_HINT_AUDIO_DEVICE_RAW_STREAM, false)) {
+            audioProps.Options = AUDCLNT_STREAMOPTIONS_RAW;
+        }
+
+        ret = IAudioClient2_SetClientProperties(client2, &audioProps);
+        if (FAILED(ret)) {
+            // This isn't fatal, let's log it instead of failing
+            SDL_LogWarn(SDL_LOG_CATEGORY_AUDIO, "IAudioClient2_SetClientProperties failed: 0x%lx", ret);
+        }
+        IAudioClient2_Release(client2);
+    }
+#endif
+
 #ifdef __IAudioClient3_INTERFACE_DEFINED__
     // Try querying IAudioClient3 if sharemode is AUDCLNT_SHAREMODE_SHARED
     if (sharemode == AUDCLNT_SHAREMODE_SHARED) {
diff --git a/test/loopwave.c b/test/loopwave.c
index 8a2d7a0ef5981..e5b55e7df9512 100644
--- a/test/loopwave.c
+++ b/test/loopwave.c
@@ -62,13 +62,17 @@ SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
 
         consumed = SDLTest_CommonArg(state, i);
         if (!consumed) {
-            if (!filename) {
+            if (SDL_strcmp(argv[i], "--role") == 0 && argv[i + 1]) {
+                SDL_SetHint(SDL_HINT_AUDIO_DEVICE_STREAM_ROLE, argv[i + 1]);
+                ++i;
+                consumed = 1;
+            } else if (!filename) {
                 filename = argv[i];
                 consumed = 1;
             }
         }
         if (consumed <= 0) {
-            static const char *options[] = { "[sample.wav]", NULL };
+            static const char *options[] = { "[--role ROLE]", "[sample.wav]", NULL };
             SDLTest_CommonLogUsage(state, argv[0], options);
             exit(1);
         }