SDL: audio: Destroy all existing SDL_AudioStreams on shutdown.

From f8fdb20d8f1d4fba4fd9f782addb9a3b987f861a Mon Sep 17 00:00:00 2001
From: "Ryan C. Gordon" <[EMAIL REDACTED]>
Date: Wed, 20 Sep 2023 10:47:11 -0400
Subject: [PATCH] audio: Destroy all existing SDL_AudioStreams on shutdown.

---
 src/audio/SDL_audio.c    | 41 +++++++++++++++++++++++++++++++++++++++-
 src/audio/SDL_audiocvt.c |  4 ++++
 src/audio/SDL_sysaudio.h | 13 ++++++++++---
 3 files changed, 54 insertions(+), 4 deletions(-)

diff --git a/src/audio/SDL_audio.c b/src/audio/SDL_audio.c
index f2a2e4d17ec7..625625f933f1 100644
--- a/src/audio/SDL_audio.c
+++ b/src/audio/SDL_audio.c
@@ -137,6 +137,42 @@ static int GetDefaultSampleFramesFromFreq(const int freq)
     }
 }
 
+void OnAudioStreamCreated(SDL_AudioStream *stream)
+{
+    SDL_assert(SDL_GetCurrentAudioDriver() != NULL);
+    SDL_assert(stream != NULL);
+
+    // this isn't really part of the "device list" but it's a convenient lock to use here.
+    SDL_LockRWLockForWriting(current_audio.device_list_lock);
+    if (current_audio.existing_streams) {
+        current_audio.existing_streams->prev = stream;
+    }
+    stream->prev = NULL;
+    stream->next = current_audio.existing_streams;
+    current_audio.existing_streams = stream;
+    SDL_UnlockRWLock(current_audio.device_list_lock);
+}
+
+void OnAudioStreamDestroy(SDL_AudioStream *stream)
+{
+    SDL_assert(SDL_GetCurrentAudioDriver() != NULL);
+    SDL_assert(stream != NULL);
+
+    // this isn't really part of the "device list" but it's a convenient lock to use here.
+    SDL_LockRWLockForWriting(current_audio.device_list_lock);
+    if (stream->prev) {
+        stream->prev->next = stream->next;
+    }
+    if (stream->next) {
+        stream->next->prev = stream->prev;
+    }
+    if (stream == current_audio.existing_streams) {
+        current_audio.existing_streams = stream->next;
+    }
+    SDL_UnlockRWLock(current_audio.device_list_lock);
+}
+
+
 // device should be locked when calling this.
 static SDL_bool AudioDeviceCanUseSimpleCopy(SDL_AudioDevice *device)
 {
@@ -657,7 +693,10 @@ void SDL_QuitAudio(void)
         return;
     }
 
-    // !!! FIXME: Destroy all known audio streams, too.
+    // Destroy any audio streams that still exist...
+    while (current_audio.existing_streams != NULL) {
+        SDL_DestroyAudioStream(current_audio.existing_streams);
+    }
 
     // merge device lists so we don't have to duplicate work below.
     SDL_LockRWLockForWriting(current_audio.device_list_lock);
diff --git a/src/audio/SDL_audiocvt.c b/src/audio/SDL_audiocvt.c
index 018701d3e891..a5be406dbf63 100644
--- a/src/audio/SDL_audiocvt.c
+++ b/src/audio/SDL_audiocvt.c
@@ -430,6 +430,8 @@ SDL_AudioStream *SDL_CreateAudioStream(const SDL_AudioSpec *src_spec, const SDL_
         return NULL;
     }
 
+    OnAudioStreamCreated(retval);
+
     if (SDL_SetAudioStreamFormat(retval, src_spec, dst_spec) == -1) {
         SDL_DestroyAudioStream(retval);
         return NULL;
@@ -1152,6 +1154,8 @@ void SDL_DestroyAudioStream(SDL_AudioStream *stream)
         return;
     }
 
+    OnAudioStreamDestroy(stream);
+
     const SDL_bool simplified = stream->simplified;
     if (simplified) {
         SDL_assert(stream->bound_device->simplified);
diff --git a/src/audio/SDL_sysaudio.h b/src/audio/SDL_sysaudio.h
index 4212abce336b..980dbc565f86 100644
--- a/src/audio/SDL_sysaudio.h
+++ b/src/audio/SDL_sysaudio.h
@@ -102,7 +102,7 @@ extern SDL_AudioDevice *SDL_FindPhysicalAudioDeviceByCallback(SDL_bool (*callbac
 extern void SDL_UpdatedAudioDeviceFormat(SDL_AudioDevice *device);
 
 // Backends can call this to get a standardized name for a thread to power a specific audio device.
-char *SDL_GetAudioThreadName(SDL_AudioDevice *device, char *buf, size_t buflen);
+extern char *SDL_GetAudioThreadName(SDL_AudioDevice *device, char *buf, size_t buflen);
 
 
 // These functions are the heart of the audio threads. Backends can call them directly if they aren't using the SDL-provided thread.
@@ -115,9 +115,12 @@ extern void SDL_CaptureAudioThreadShutdown(SDL_AudioDevice *device);
 extern void SDL_AudioThreadFinalize(SDL_AudioDevice *device);
 
 // this gets used from the audio device threads. It has rules, don't use this if you don't know how to use it!
-void ConvertAudio(int num_frames, const void *src, SDL_AudioFormat src_format, int src_channels,
-                  void *dst, SDL_AudioFormat dst_format, int dst_channels, void* scratch);
+extern void ConvertAudio(int num_frames, const void *src, SDL_AudioFormat src_format, int src_channels,
+                         void *dst, SDL_AudioFormat dst_format, int dst_channels, void* scratch);
 
+// Special case to let something in SDL_audiocvt.c access something in SDL_audio.c. Don't use this.
+extern void OnAudioStreamCreated(SDL_AudioStream *stream);
+extern void OnAudioStreamDestroy(SDL_AudioStream *stream);
 
 typedef struct SDL_AudioDriverImpl
 {
@@ -151,6 +154,7 @@ typedef struct SDL_AudioDriver
     SDL_RWLock *device_list_lock;  // A mutex for device detection
     SDL_AudioDevice *output_devices;  // the list of currently-available audio output devices.
     SDL_AudioDevice *capture_devices;  // the list of currently-available audio capture devices.
+    SDL_AudioStream *existing_streams;  // a list of all existing SDL_AudioStreams.
     SDL_AudioDeviceID default_output_device_id;
     SDL_AudioDeviceID default_capture_device_id;
     SDL_AtomicInt output_device_count;
@@ -191,6 +195,9 @@ struct SDL_AudioStream
     SDL_LogicalAudioDevice *bound_device;
     SDL_AudioStream *next_binding;
     SDL_AudioStream *prev_binding;
+
+    SDL_AudioStream *prev;  // linked list of all existing streams (so we can free them on shutdown).
+    SDL_AudioStream *next;  // linked list of all existing streams (so we can free them on shutdown).
 };
 
 /* Logical devices are an abstraction in SDL3; you can open the same physical