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());