SDL_mixer: api: Added MIX_LoadAudioNoCopy.

From 4468791ac00c70118a41487b8f5d8ce1781b1815 Mon Sep 17 00:00:00 2001
From: "Ryan C. Gordon" <[EMAIL REDACTED]>
Date: Tue, 3 Mar 2026 22:47:57 -0500
Subject: [PATCH] api: Added MIX_LoadAudioNoCopy.

Fixes #824.
---
 include/SDL3_mixer/SDL_mixer.h | 61 +++++++++++++++++++++++++++++++++-
 src/SDL_mixer.c                | 31 +++++++++++++++++
 src/SDL_mixer.exports          |  1 +
 src/SDL_mixer.sym              |  1 +
 test/testmixer.c               | 11 ++++++
 5 files changed, 104 insertions(+), 1 deletion(-)

diff --git a/include/SDL3_mixer/SDL_mixer.h b/include/SDL3_mixer/SDL_mixer.h
index 63ec171a..4aac37a4 100644
--- a/include/SDL3_mixer/SDL_mixer.h
+++ b/include/SDL3_mixer/SDL_mixer.h
@@ -609,6 +609,65 @@ extern SDL_DECLSPEC MIX_Audio * SDLCALL MIX_LoadAudio_IO(MIX_Mixer *mixer, SDL_I
  */
 extern SDL_DECLSPEC MIX_Audio * SDLCALL MIX_LoadAudio(MIX_Mixer *mixer, const char *path, bool predecode);
 
+/**
+ * Load audio for playback from a memory buffer without making a copy.
+ *
+ * When loading audio through most other LoadAudio functions, the data will be
+ * cached fully in RAM in its original data format, for decoding on-demand.
+ * This function does most of the same work as those functions, but instead
+ * uses a buffer of memory provided by the app that it does not make a copy
+ * of.
+ *
+ * This buffer must live for the entire time the returned MIX_Audio lives, as
+ * the mixer will access the buffer whenever it needs to mix more data.
+ *
+ * This function is meant to maximize efficiency: if the data is already in
+ * memory and can remain there, don't copy it. This data can be in any
+ * supported audio file format (WAV, MP3, etc); it will be decoded on the
+ * fly while mixing. Unlike MIX_LoadAudio(), there is no `predecode` option
+ * offered here, as this is meant to optimize for data that's already in
+ * memory and intends to exist there for significant time; since predecoding
+ * would only need the file format data once, upfront, one could simply wrap
+ * it in SDL_CreateIOFromConstMem() and pass that to MIX_LoadAudio_IO().
+ *
+ * MIX_Audio objects can be shared between multiple mixers. The `mixer`
+ * parameter just suggests the most likely mixer to use this audio, in case
+ * some optimization might be applied, but this is not required, and a NULL
+ * mixer may be specified.
+ *
+ * If `free_when_done` is true, SDL_mixer will call `SDL_free(data)` when the
+ * returned MIX_Audio is eventually destroyed. This can be useful when the
+ * data is not static, but rather loaded elsewhere for this specific
+ * MIX_Audio and simply wants to avoid the extra copy.
+ *
+ * As audio format information is obtained from the file format metadata,
+ * this isn't useful for raw PCM data; in that case, use
+ * MIX_LoadRawAudioNoCopy() instead, which offers an SDL_AudioSpec.
+ *
+ * Once a MIX_Audio is created, it can be assigned to a MIX_Track with
+ * MIX_SetTrackAudio(), or played without any management with MIX_PlayAudio().
+ *
+ * When done with a MIX_Audio, it can be freed with MIX_DestroyAudio().
+ *
+ * \param mixer a mixer this audio is intended to be used with. May be NULL.
+ * \param data the buffer where the audio data lives.
+ * \param datalen the size, in bytes, of the buffer.
+ * \param free_when_done if true, `data` will be given to SDL_free() when the
+ *                       MIX_Audio is destroyed.
+ * \returns an audio object that can be used to make sound on a mixer, or NULL
+ *          on failure; call SDL_GetError() for more information.
+ *
+ * \threadsafety It is safe to call this function from any thread.
+ *
+ * \since This function is available since SDL_mixer 3.0.0.
+ *
+ * \sa MIX_DestroyAudio
+ * \sa MIX_SetTrackAudio
+ * \sa MIX_LoadRawAudioNoCopy
+ * \sa MIX_LoadAudio_IO
+ */
+extern SDL_DECLSPEC MIX_Audio * SDLCALL MIX_LoadAudioNoCopy(MIX_Mixer *mixer, const void *data, size_t datalen, bool free_when_done);
+
 /**
  * Load audio for playback through a collection of properties.
  *
@@ -740,7 +799,7 @@ extern SDL_DECLSPEC MIX_Audio * SDLCALL MIX_LoadRawAudio(MIX_Mixer *mixer, const
  * Load raw PCM data from a memory buffer without making a copy.
  *
  * This buffer must live for the entire time the returned MIX_Audio lives, as
- * it will access it whenever it needs to mix more data.
+ * the mixer will access the buffer whenever it needs to mix more data.
  *
  * This function is meant to maximize efficiency: if the data is already in
  * memory and can remain there, don't copy it. But it can also lead to some
diff --git a/src/SDL_mixer.c b/src/SDL_mixer.c
index 27fc5207..3de3a47b 100644
--- a/src/SDL_mixer.c
+++ b/src/SDL_mixer.c
@@ -1185,6 +1185,37 @@ MIX_Audio *MIX_LoadAudio(MIX_Mixer *mixer, const char *path, bool predecode)
     return retval;
 }
 
+MIX_Audio *MIX_LoadAudioNoCopy(MIX_Mixer *mixer, const void *data, size_t datalen, bool free_when_done)
+{
+    if (!data) {
+        SDL_InvalidParamError("data");
+        return NULL;
+    }
+
+    SDL_IOStream *io = SDL_IOFromConstMem(data, datalen);
+    if (!io) {
+        return NULL;
+    }
+
+    const SDL_PropertiesID props = SDL_CreateProperties();
+    SDL_SetPointerProperty(props, MIX_PROP_AUDIO_LOAD_PREFERRED_MIXER_POINTER, mixer);
+    SDL_SetPointerProperty(props, MIX_PROP_AUDIO_LOAD_IOSTREAM_POINTER, io);
+    SDL_SetBooleanProperty(props, MIX_PROP_AUDIO_LOAD_CLOSEIO_BOOLEAN, true);
+    SDL_SetBooleanProperty(props, MIX_PROP_AUDIO_LOAD_ONDEMAND_BOOLEAN, true);  // so it doesn't make a copy to precache
+    MIX_Audio *audio = MIX_LoadAudioWithProperties(props);
+    SDL_DestroyProperties(props);
+
+    if (!audio) {
+        return NULL;
+    }
+
+    audio->precache = data;
+    audio->precachelen = datalen;
+    audio->free_precache = free_when_done;
+
+    return audio;
+}
+
 MIX_Audio *MIX_LoadRawAudio_IO(MIX_Mixer *mixer, SDL_IOStream *io, const SDL_AudioSpec *spec, bool closeio)
 {
     if (!CheckInitialized()) {
diff --git a/src/SDL_mixer.exports b/src/SDL_mixer.exports
index 9b8a7369..fba7ff00 100644
--- a/src/SDL_mixer.exports
+++ b/src/SDL_mixer.exports
@@ -90,4 +90,5 @@ _MIX_GetTrackTags
 _MIX_GetTaggedTracks
 _MIX_SetMixerFrequencyRatio
 _MIX_GetMixerFrequencyRatio
+_MIX_LoadAudioNoCopy
 # extra symbols go here (don't modify this line)
diff --git a/src/SDL_mixer.sym b/src/SDL_mixer.sym
index febfe92c..719c443f 100644
--- a/src/SDL_mixer.sym
+++ b/src/SDL_mixer.sym
@@ -91,6 +91,7 @@ SDL3_mixer_0.0.0 {
     MIX_GetTaggedTracks;
     MIX_SetMixerFrequencyRatio;
     MIX_GetMixerFrequencyRatio;
+    MIX_LoadAudioNoCopy;
     # extra symbols go here (don't modify this line)
   local: *;
 };
diff --git a/test/testmixer.c b/test/testmixer.c
index 1ca084cb..8d002e96 100644
--- a/test/testmixer.c
+++ b/test/testmixer.c
@@ -158,7 +158,18 @@ SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
     SDL_Log("%s", "");
 
     const char *audiofname = argv[1];
+#if 1
     MIX_Audio *audio = MIX_LoadAudio(mixer, audiofname, false);
+#else
+    size_t databuffersize = 0;
+    void *databuffer = SDL_LoadFile(audiofname, &databuffersize);
+    if (!databuffer) {
+        SDL_Log("Failed to load file '%s' from disk: %s", audiofname, SDL_GetError());
+        return SDL_APP_FAILURE;
+    }
+    MIX_Audio *audio = MIX_LoadAudioNoCopy(mixer, databuffer, databuffersize, true);
+#endif
+
     //MIX_Audio *audio = MIX_CreateSineWaveAudio(mixer, 300, 0.25f, 5000);
     if (!audio) {
         SDL_Log("Failed to load '%s': %s", audiofname, SDL_GetError());