SDL: audio: Added SDL_PutAudioStreamDataNoCopy.

From 5e50d39b382a457fa46313b3b212309c62c29682 Mon Sep 17 00:00:00 2001
From: "Ryan C. Gordon" <[EMAIL REDACTED]>
Date: Sat, 24 May 2025 22:52:22 -0400
Subject: [PATCH] audio: Added SDL_PutAudioStreamDataNoCopy.

---
 include/SDL3/SDL_audio.h          | 76 +++++++++++++++++++++++++++++++
 src/audio/SDL_audiocvt.c          | 31 ++++++++++---
 src/audio/SDL_audioqueue.h        |  2 +-
 src/dynapi/SDL_dynapi.sym         |  1 +
 src/dynapi/SDL_dynapi_overrides.h |  1 +
 src/dynapi/SDL_dynapi_procs.h     |  1 +
 6 files changed, 104 insertions(+), 8 deletions(-)

diff --git a/include/SDL3/SDL_audio.h b/include/SDL3/SDL_audio.h
index c6a5dbc691b07..b90887d124c4b 100644
--- a/include/SDL3/SDL_audio.h
+++ b/include/SDL3/SDL_audio.h
@@ -1414,6 +1414,82 @@ extern SDL_DECLSPEC bool SDLCALL SDL_SetAudioStreamOutputChannelMap(SDL_AudioStr
  */
 extern SDL_DECLSPEC bool SDLCALL SDL_PutAudioStreamData(SDL_AudioStream *stream, const void *buf, int len);
 
+/**
+ * A callback that fires for completed SDL_PutAudioStreamDataNoCopy() data.
+ *
+ * When using SDL_PutAudioStreamDataNoCopy() to provide data to an
+ * SDL_AudioStream, it's not safe to dispose of the data until the stream
+ * has completely consumed it. Often times it's difficult to know exactly
+ * when this has happened.
+ *
+ * This callback fires once when the stream no longer needs the buffer,
+ * allowing the app to easily free or reuse it.
+ *
+ * \param userdata an opaque pointer provided by the app for their personal
+ *                 use.
+ * \param buf the pointer provided to SDL_PutAudioStreamDataNoCopy().
+ * \param buflen the size of buffer, in bytes, provided to
+ *               SDL_PutAudioStreamDataNoCopy().
+ *
+ * \threadsafety This callbacks may run from any thread, so if you need to
+ *               protect shared data, you should use SDL_LockAudioStream to
+ *               serialize access; this lock will be held before your callback
+ *               is called, so your callback does not need to manage the lock
+ *               explicitly.
+ *
+ * \since This datatype is available since SDL 3.4.0.
+ *
+ * \sa SDL_SetAudioStreamGetCallback
+ * \sa SDL_SetAudioStreamPutCallback
+ */
+typedef void (SDLCALL *SDL_AudioStreamDataCompleteCallback)(void *userdata, const void *buf, int buflen);
+
+/**
+ * Add constant data to the stream.
+ *
+ * Unlike SDL_PutAudioStreamData(), this function does not make a copy of the
+ * provided data, instead storing the provided pointer. This means that the
+ * put operation does not need to allocate and copy the data, but the original
+ * data must remain available until the stream is done with it, either by
+ * being read from the stream in its entirety, or a call to
+ * SDL_ClearAudioStream() or SDL_DestroyAudioStream().
+ *
+ * The data must match the format/channels/samplerate specified in the latest
+ * call to SDL_SetAudioStreamFormat, or the format specified when creating the
+ * stream if it hasn't been changed.
+ *
+ * An optional callback may be provided, which is called when the stream no
+ * longer needs the data. Once this callback fires, the stream will not
+ * access the data again.
+ *
+ * Note that there is still an allocation to store tracking information,
+ * so this function is more efficient for larger blocks of data. If you're
+ * planning to put a few samples at a time, it will be more efficient to use
+ * SDL_PutAudioStreamData(), which allocates and buffers in blocks.
+ *
+ * \param stream the stream the audio data is being added to.
+ * \param buf a pointer to the audio data to add.
+ * \param len the number of bytes to write to the stream.
+ * \param callback the callback function to call when the data is no longer
+ *                 needed by the stream. May be NULL.
+ * \param userdata an opaque pointer provided to the callback for its own
+ *                 personal use.
+ * \returns true on success or false on failure; call SDL_GetError() for more
+ *          information.
+ *
+ * \threadsafety It is safe to call this function from any thread, but if the
+ *               stream has a callback set, the caller might need to manage
+ *               extra locking.
+ *
+ * \since This function is available since SDL 3.4.0.
+ *
+ * \sa SDL_ClearAudioStream
+ * \sa SDL_FlushAudioStream
+ * \sa SDL_GetAudioStreamData
+ * \sa SDL_GetAudioStreamQueued
+ */
+extern SDL_DECLSPEC bool SDLCALL SDL_PutAudioStreamDataNoCopy(SDL_AudioStream *stream, const void *buf, int len, SDL_AudioStreamDataCompleteCallback callback, void *userdata);
+
 /**
  * Add data to the stream with each channel in a separate array.
  *
diff --git a/src/audio/SDL_audiocvt.c b/src/audio/SDL_audiocvt.c
index d05fd915aa009..13888fd2469b3 100644
--- a/src/audio/SDL_audiocvt.c
+++ b/src/audio/SDL_audiocvt.c
@@ -1026,6 +1026,29 @@ bool SDL_PutAudioStreamPlanarData(SDL_AudioStream *stream, const void * const *c
     return retval;
 }
 
+static void SDLCALL DontFreeThisAudioBuffer(void *userdata, const void *buf, int len)
+{
+    // We don't own the buffer, but know it will outlive the stream
+}
+
+bool SDL_PutAudioStreamDataNoCopy(SDL_AudioStream *stream, const void *buf, int len, SDL_AudioStreamDataCompleteCallback callback, void *userdata)
+{
+    if (!stream) {
+        return SDL_InvalidParamError("stream");
+    } else if (!buf) {
+        return SDL_InvalidParamError("buf");
+    } else if (len < 0) {
+        return SDL_InvalidParamError("len");
+    } else if (len == 0) {
+        if (callback) {
+            callback(userdata, buf, len);
+        }
+        return true; // nothing to do.
+    }
+
+    return PutAudioStreamBuffer(stream, buf, len, callback ? callback : DontFreeThisAudioBuffer, userdata);
+}
+
 bool SDL_FlushAudioStream(SDL_AudioStream *stream)
 {
     if (!stream) {
@@ -1483,11 +1506,6 @@ void SDL_DestroyAudioStream(SDL_AudioStream *stream)
     SDL_free(stream);
 }
 
-static void SDLCALL DontFreeThisAudioBuffer(void *userdata, const void *buf, int len)
-{
-    // We don't own the buffer, but know it will outlive the stream
-}
-
 bool SDL_ConvertAudioSamples(const SDL_AudioSpec *src_spec, const Uint8 *src_data, int src_len, const SDL_AudioSpec *dst_spec, Uint8 **dst_data, int *dst_len)
 {
     if (dst_data) {
@@ -1514,8 +1532,7 @@ bool SDL_ConvertAudioSamples(const SDL_AudioSpec *src_spec, const Uint8 *src_dat
 
     SDL_AudioStream *stream = SDL_CreateAudioStream(src_spec, dst_spec);
     if (stream) {
-        if (PutAudioStreamBuffer(stream, src_data, src_len, DontFreeThisAudioBuffer, NULL) &&
-            SDL_FlushAudioStream(stream)) {
+        if (SDL_PutAudioStreamDataNoCopy(stream, src_data, src_len, NULL, NULL) && SDL_FlushAudioStream(stream)) {
             dstlen = SDL_GetAudioStreamAvailable(stream);
             if (dstlen >= 0) {
                 dst = (Uint8 *)SDL_malloc(dstlen);
diff --git a/src/audio/SDL_audioqueue.h b/src/audio/SDL_audioqueue.h
index 466294622a045..dc3c71c20c637 100644
--- a/src/audio/SDL_audioqueue.h
+++ b/src/audio/SDL_audioqueue.h
@@ -25,7 +25,7 @@
 
 // Internal functions used by SDL_AudioStream for queueing audio.
 
-typedef void (SDLCALL *SDL_ReleaseAudioBufferCallback)(void *userdata, const void *buffer, int buflen);
+typedef SDL_AudioStreamDataCompleteCallback SDL_ReleaseAudioBufferCallback;
 
 typedef struct SDL_AudioQueue SDL_AudioQueue;
 typedef struct SDL_AudioTrack SDL_AudioTrack;
diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym
index a3e47763ea072..91bd4300c732f 100644
--- a/src/dynapi/SDL_dynapi.sym
+++ b/src/dynapi/SDL_dynapi.sym
@@ -1253,6 +1253,7 @@ SDL3_0.0.0 {
     SDL_PutAudioStreamPlanarData;
     SDL_SetAudioIterationCallbacks;
     SDL_GetEventDescription;
+    SDL_PutAudioStreamDataNoCopy;
     # extra symbols go here (don't modify this line)
   local: *;
 };
diff --git a/src/dynapi/SDL_dynapi_overrides.h b/src/dynapi/SDL_dynapi_overrides.h
index 4242dab7e97b8..fcc0bad7b25ff 100644
--- a/src/dynapi/SDL_dynapi_overrides.h
+++ b/src/dynapi/SDL_dynapi_overrides.h
@@ -1278,3 +1278,4 @@
 #define SDL_PutAudioStreamPlanarData SDL_PutAudioStreamPlanarData_REAL
 #define SDL_SetAudioIterationCallbacks SDL_SetAudioIterationCallbacks_REAL
 #define SDL_GetEventDescription SDL_GetEventDescription_REAL
+#define SDL_PutAudioStreamDataNoCopy SDL_PutAudioStreamDataNoCopy_REAL
diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h
index 49187b03292fe..60e0dfea5728f 100644
--- a/src/dynapi/SDL_dynapi_procs.h
+++ b/src/dynapi/SDL_dynapi_procs.h
@@ -1286,3 +1286,4 @@ SDL_DYNAPI_PROC(SDL_Renderer*,SDL_CreateGPURenderer,(SDL_Window *a,SDL_GPUShader
 SDL_DYNAPI_PROC(bool,SDL_PutAudioStreamPlanarData,(SDL_AudioStream *a,const void * const*b,int c,int d),(a,b,c,d),return)
 SDL_DYNAPI_PROC(bool,SDL_SetAudioIterationCallbacks,(SDL_AudioDeviceID a,SDL_AudioIterationCallback b,SDL_AudioIterationCallback c,void *d),(a,b,c,d),return)
 SDL_DYNAPI_PROC(int,SDL_GetEventDescription,(const SDL_Event *a,char *b,int c),(a,b,c),return)
+SDL_DYNAPI_PROC(bool,SDL_PutAudioStreamDataNoCopy,(SDL_AudioStream *a,const void *b,int c,SDL_AudioStreamDataCompleteCallback d,void *e),(a,b,c,d,e),return)