SDL: audio: Added SDL_EVENT_AUDIO_DEVICE_FORMAT_CHANGED

From 23206b9e3ffec6aea97bbc3888fe260baa445848 Mon Sep 17 00:00:00 2001
From: "Ryan C. Gordon" <[EMAIL REDACTED]>
Date: Tue, 19 Sep 2023 17:54:44 -0400
Subject: [PATCH] audio: Added SDL_EVENT_AUDIO_DEVICE_FORMAT_CHANGED

This fires if an opened device changes formats (which it can on Windows,
if the user changes this in the system control panel, and WASAPI can
report), or if a default device migrates to new hardware and the format
doesn't match.

This will fire for all logical devices on a physical device (and if it's
a format change and not a default device change, it'll fire for the
physical device too, but that's honestly not that useful and might change).

Fixes #8267.
---
 include/SDL3/SDL_events.h |  9 ++++----
 src/audio/SDL_audio.c     | 45 +++++++++++++++++++++++++++++++++------
 src/audio/SDL_audiocvt.c  |  2 --
 src/audio/SDL_sysaudio.h  |  2 ++
 src/events/SDL_events.c   |  3 +++
 5 files changed, 48 insertions(+), 13 deletions(-)

diff --git a/include/SDL3/SDL_events.h b/include/SDL3/SDL_events.h
index f21e25161f90..a3c5cced02de 100644
--- a/include/SDL3/SDL_events.h
+++ b/include/SDL3/SDL_events.h
@@ -185,8 +185,9 @@ typedef enum
     SDL_EVENT_DROP_POSITION,             /**< Position while moving over the window */
 
     /* Audio hotplug events */
-    SDL_EVENT_AUDIO_DEVICE_ADDED = 0x1100, /**< A new audio device is available */
-    SDL_EVENT_AUDIO_DEVICE_REMOVED,        /**< An audio device has been removed. */
+    SDL_EVENT_AUDIO_DEVICE_ADDED = 0x1100,  /**< A new audio device is available */
+    SDL_EVENT_AUDIO_DEVICE_REMOVED,         /**< An audio device has been removed. */
+    SDL_EVENT_AUDIO_DEVICE_FORMAT_CHANGED,  /**< An audio device's format has been changed by the system. */
 
     /* Sensor events */
     SDL_EVENT_SENSOR_UPDATE = 0x1200,     /**< A sensor was updated */
@@ -491,9 +492,9 @@ typedef struct SDL_GamepadSensorEvent
  */
 typedef struct SDL_AudioDeviceEvent
 {
-    Uint32 type;        /**< ::SDL_EVENT_AUDIO_DEVICE_ADDED, or ::SDL_EVENT_AUDIO_DEVICE_REMOVED */
+    Uint32 type;        /**< ::SDL_EVENT_AUDIO_DEVICE_ADDED, or ::SDL_EVENT_AUDIO_DEVICE_REMOVED, or ::SDL_EVENT_AUDIO_DEVICE_FORMAT_CHANGED */
     Uint64 timestamp;   /**< In nanoseconds, populated using SDL_GetTicksNS() */
-    SDL_AudioDeviceID which;       /**< SDL_AudioDeviceID for the device being added or removed */
+    SDL_AudioDeviceID which;       /**< SDL_AudioDeviceID for the device being added or removed or changing */
     Uint8 iscapture;    /**< zero if an output device, non-zero if a capture device. */
     Uint8 padding1;
     Uint8 padding2;
diff --git a/src/audio/SDL_audio.c b/src/audio/SDL_audio.c
index 52ee69d26a03..f2a2e4d17ec7 100644
--- a/src/audio/SDL_audio.c
+++ b/src/audio/SDL_audio.c
@@ -1797,6 +1797,7 @@ void SDL_DefaultAudioDeviceChanged(SDL_AudioDevice *new_default_device)
         SDL_AudioSpec spec;
         SDL_bool needs_migration = SDL_FALSE;
         SDL_zero(spec);
+
         for (SDL_LogicalAudioDevice *logdev = current_default_device->logical_devices; logdev != NULL; logdev = logdev->next) {
             if (logdev->opened_as_default) {
                 needs_migration = SDL_TRUE;
@@ -1824,6 +1825,8 @@ void SDL_DefaultAudioDeviceChanged(SDL_AudioDevice *new_default_device)
         }
 
         if (needs_migration) {
+            const SDL_bool spec_changed = !AUDIO_SPECS_EQUAL(current_default_device->spec, new_default_device->spec);
+            const SDL_bool post_fmt_event = (spec_changed && SDL_EventEnabled(SDL_EVENT_AUDIO_DEVICE_FORMAT_CHANGED)) ? SDL_TRUE : SDL_FALSE;
             SDL_LogicalAudioDevice *next = NULL;
             for (SDL_LogicalAudioDevice *logdev = current_default_device->logical_devices; logdev != NULL; logdev = next) {
                 next = logdev->next;
@@ -1852,6 +1855,17 @@ void SDL_DefaultAudioDeviceChanged(SDL_AudioDevice *new_default_device)
                 logdev->prev = NULL;
                 logdev->next = new_default_device->logical_devices;
                 new_default_device->logical_devices = logdev;
+
+                // Post an event for each logical device we moved.
+                if (post_fmt_event) {
+                    SDL_Event event;
+                    SDL_zero(event);
+                    event.type = SDL_EVENT_AUDIO_DEVICE_FORMAT_CHANGED;
+                    event.common.timestamp = 0;
+                    event.adevice.iscapture = iscapture ? 1 : 0;
+                    event.adevice.which = logdev->instance_id;
+                    SDL_PushEvent(&event);
+                }
             }
 
             current_default_device->simple_copy = AudioDeviceCanUseSimpleCopy(current_default_device);
@@ -1883,13 +1897,15 @@ int SDL_AudioDeviceFormatChangedAlreadyLocked(SDL_AudioDevice *device, const SDL
     const int orig_work_buffer_size = device->work_buffer_size;
     const SDL_bool iscapture = device->iscapture;
 
-    if ((device->spec.format != newspec->format) || (device->spec.channels != newspec->channels) || (device->spec.freq != newspec->freq)) {
-        SDL_memcpy(&device->spec, newspec, sizeof (*newspec));
-        for (SDL_LogicalAudioDevice *logdev = device->logical_devices; !kill_device && (logdev != NULL); logdev = logdev->next) {
-            for (SDL_AudioStream *stream = logdev->bound_streams; !kill_device && (stream != NULL); stream = stream->next_binding) {
-                if (SDL_SetAudioStreamFormat(stream, iscapture ? &device->spec : NULL, iscapture ? NULL : &device->spec) == -1) {
-                    kill_device = SDL_TRUE;
-                }
+    if (AUDIO_SPECS_EQUAL(device->spec, *newspec)) {
+        return 0;  // we're already in that format.
+    }
+
+    SDL_memcpy(&device->spec, newspec, sizeof (*newspec));
+    for (SDL_LogicalAudioDevice *logdev = device->logical_devices; !kill_device && (logdev != NULL); logdev = logdev->next) {
+        for (SDL_AudioStream *stream = logdev->bound_streams; !kill_device && (stream != NULL); stream = stream->next_binding) {
+            if (SDL_SetAudioStreamFormat(stream, iscapture ? &device->spec : NULL, iscapture ? NULL : &device->spec) == -1) {
+                kill_device = SDL_TRUE;
             }
         }
     }
@@ -1923,6 +1939,21 @@ int SDL_AudioDeviceFormatChangedAlreadyLocked(SDL_AudioDevice *device, const SDL
         }
     }
 
+    // Post an event for the physical device, and each logical device on this physical device.
+    if (!kill_device && SDL_EventEnabled(SDL_EVENT_AUDIO_DEVICE_FORMAT_CHANGED)) {
+        SDL_Event event;
+        SDL_zero(event);
+        event.type = SDL_EVENT_AUDIO_DEVICE_FORMAT_CHANGED;
+        event.common.timestamp = 0;
+        event.adevice.iscapture = device->iscapture ? 1 : 0;
+        event.adevice.which = device->instance_id;
+        SDL_PushEvent(&event);
+        for (SDL_LogicalAudioDevice *logdev = device->logical_devices; logdev != NULL; logdev = logdev->next) {
+            event.adevice.which = logdev->instance_id;
+            SDL_PushEvent(&event);
+        }
+    }
+
     return kill_device ? -1 : 0;
 }
 
diff --git a/src/audio/SDL_audiocvt.c b/src/audio/SDL_audiocvt.c
index fa680974dc4c..eefc61101a47 100644
--- a/src/audio/SDL_audiocvt.c
+++ b/src/audio/SDL_audiocvt.c
@@ -29,8 +29,6 @@
 #define SDL_INT_MAX ((int)(~0u>>1))
 #endif
 
-#define AUDIO_SPECS_EQUAL(x, y) (((x).format == (y).format) && ((x).channels == (y).channels) && ((x).freq == (y).freq))
-
 /*
  * CHANNEL LAYOUTS AS SDL EXPECTS THEM:
  *
diff --git a/src/audio/SDL_sysaudio.h b/src/audio/SDL_sysaudio.h
index 3c859d83cdf1..656397831429 100644
--- a/src/audio/SDL_sysaudio.h
+++ b/src/audio/SDL_sysaudio.h
@@ -56,6 +56,8 @@ extern void (*SDL_Convert_F32_to_S32)(Sint32 *dst, const float *src, int num_sam
 #define DEFAULT_AUDIO_CAPTURE_CHANNELS 1
 #define DEFAULT_AUDIO_CAPTURE_FREQUENCY 44100
 
+#define AUDIO_SPECS_EQUAL(x, y) (((x).format == (y).format) && ((x).channels == (y).channels) && ((x).freq == (y).freq))
+
 typedef struct SDL_AudioDevice SDL_AudioDevice;
 typedef struct SDL_LogicalAudioDevice SDL_LogicalAudioDevice;
 
diff --git a/src/events/SDL_events.c b/src/events/SDL_events.c
index ac692faad7a8..24f40c6d5555 100644
--- a/src/events/SDL_events.c
+++ b/src/events/SDL_events.c
@@ -440,6 +440,9 @@ static void SDL_LogEvent(const SDL_Event *event)
         SDL_EVENT_CASE(SDL_EVENT_AUDIO_DEVICE_REMOVED)
         PRINT_AUDIODEV_EVENT(event);
         break;
+        SDL_EVENT_CASE(SDL_EVENT_AUDIO_DEVICE_FORMAT_CHANGED)
+        PRINT_AUDIODEV_EVENT(event);
+        break;
 #undef PRINT_AUDIODEV_EVENT
 
         SDL_EVENT_CASE(SDL_EVENT_SENSOR_UPDATE)