From ddfbf1e86a6ea0dff4d7f5b9b45b3ef3286009d5 Mon Sep 17 00:00:00 2001
From: "Ryan C. Gordon" <[EMAIL REDACTED]>
Date: Thu, 5 Mar 2026 08:46:49 -0500
Subject: [PATCH] api: Added MIX_LockMixer and MIX_UnlockMixer.
---
docs/README-migration.md | 10 ++++++
include/SDL3_mixer/SDL_mixer.h | 60 ++++++++++++++++++++++++++++++++++
src/SDL_mixer.c | 14 ++++++++
src/SDL_mixer.exports | 2 ++
src/SDL_mixer.sym | 2 ++
5 files changed, 88 insertions(+)
diff --git a/docs/README-migration.md b/docs/README-migration.md
index ba76861e..1c4a253b 100644
--- a/docs/README-migration.md
+++ b/docs/README-migration.md
@@ -102,6 +102,16 @@ is well-prepared to manage any input or output formats it ends up with.
Mix_QuerySpec() has been replaced by MIX_GetMixerFormat().
+
+## Locking
+
+SDL2_mixer expected you to lock the SDL audio device directly if you needed
+explicit synchronization. SDL3 doesn't have a direct equivalent of SDL2's
+SDL_LockAudioDevice(), and SDL3_mixer might be operating without an audio
+device at all, so there are now explicit MIX_LockMixer() and MIX_UnlockMixer()
+API calls available.
+
+
## Music vs chunks
SDL2_mixer made a distinction between "chunks" (uncompressed audio sitting in
diff --git a/include/SDL3_mixer/SDL_mixer.h b/include/SDL3_mixer/SDL_mixer.h
index 2bab24cc..6ec025f1 100644
--- a/include/SDL3_mixer/SDL_mixer.h
+++ b/include/SDL3_mixer/SDL_mixer.h
@@ -520,6 +520,66 @@ extern SDL_DECLSPEC SDL_PropertiesID SDLCALL MIX_GetMixerProperties(MIX_Mixer *m
*/
extern SDL_DECLSPEC bool SDLCALL MIX_GetMixerFormat(MIX_Mixer *mixer, SDL_AudioSpec *spec);
+/**
+ * Lock a mixer by obtaining its internal mutex.
+ *
+ * While locked, the mixer will not be able to mix more audio or change its
+ * internal state in another thread. Those other threads will block until the
+ * mixer is unlocked again.
+ *
+ * Under the hood, this function calls SDL_LockMutex(), so all the same rules
+ * apply: the lock can be recursive, it must be unlocked the same number of
+ * times from the same thread that locked it, etc.
+ *
+ * Just about every SDL_mixer API _also_ locks the mixer while doing its work,
+ * as does the SDL audio device thread while actual mixing is in progress, so
+ * basic use of this library never requires the app to explicitly lock the
+ * device to be thread safe. There are two scenarios where this can be useful,
+ * however:
+ *
+ * - The app has a provided a callback that the mixing thread might call, and
+ * there is some app state that needs to be protected against race
+ * conditions as changes are made and mixing progresses simultaneously. Any
+ * lock can be used for this, but this is a conveniently-available lock.
+ * - The app wants to make multiple, atomic changes to the mix. For example,
+ * to start several tracks at the exact same moment, one would lock the
+ * mixer, call MIX_PlayTrack multiple times, and then unlock again; all the
+ * tracks will start mixing on the same sample frame.
+ *
+ * Do not lock the mixer for significant amounts of time, or it can cause
+ * audio dropouts. Just do simply things quickly and unlock again.
+ *
+ * Locking a NULL mixer is a safe no-op.
+ *
+ * \param mixer the mixer to lock. May be NULL.
+ *
+ * \threadsafety It is safe to call this function from any thread.
+ *
+ * \since This function is available since SDL_mixer 3.0.0.
+ */
+extern SDL_DECLSPEC void SDLCALL MIX_LockMixer(MIX_Mixer *mixer);
+
+/**
+ * Unlock a mixer previously locked by a call to MIX_LockMixer().
+ *
+ * While locked, the mixer will not be able to mix more audio or change its
+ * internal state another thread. Those other threads will block until the
+ * mixer is unlocked again.
+ *
+ * Under the hood, this function calls SDL_LockMutex(), so all the same rules
+ * apply: the lock can be recursive, it must be unlocked the same number of
+ * times from the same thread that locked it, etc.
+ *
+ * Unlocking a NULL mixer is a safe no-op.
+ *
+ * \param mixer the mixer to unlock. May be NULL.
+ *
+ * \threadsafety It is safe to call this function from any thread.
+ *
+ * \since This function is available since SDL_mixer 3.0.0.
+ */
+extern SDL_DECLSPEC void SDLCALL MIX_UnlockMixer(MIX_Mixer *mixer);
+
/**
* Load audio for playback from an SDL_IOStream.
*
diff --git a/src/SDL_mixer.c b/src/SDL_mixer.c
index fc387de6..4630e5f1 100644
--- a/src/SDL_mixer.c
+++ b/src/SDL_mixer.c
@@ -926,6 +926,20 @@ bool MIX_GetMixerFormat(MIX_Mixer *mixer, SDL_AudioSpec *spec)
return SDL_GetAudioStreamFormat(mixer->output_stream, NULL, spec);
}
+void MIX_LockMixer(MIX_Mixer *mixer)
+{
+ if (mixer) {
+ LockMixer(mixer);
+ }
+}
+
+void MIX_UnlockMixer(MIX_Mixer *mixer)
+{
+ if (mixer) {
+ UnlockMixer(mixer);
+ }
+}
+
static const MIX_Decoder *PrepareDecoder(SDL_IOStream *io, MIX_Audio *audio)
{
const char *decoder_name = SDL_GetStringProperty(audio->props, MIX_PROP_AUDIO_DECODER_STRING, NULL);
diff --git a/src/SDL_mixer.exports b/src/SDL_mixer.exports
index fba7ff00..2b2cbe7d 100644
--- a/src/SDL_mixer.exports
+++ b/src/SDL_mixer.exports
@@ -91,4 +91,6 @@ _MIX_GetTaggedTracks
_MIX_SetMixerFrequencyRatio
_MIX_GetMixerFrequencyRatio
_MIX_LoadAudioNoCopy
+_MIX_LockMixer
+_MIX_UnlockMixer
# extra symbols go here (don't modify this line)
diff --git a/src/SDL_mixer.sym b/src/SDL_mixer.sym
index 719c443f..e311a6a6 100644
--- a/src/SDL_mixer.sym
+++ b/src/SDL_mixer.sym
@@ -92,6 +92,8 @@ SDL3_mixer_0.0.0 {
MIX_SetMixerFrequencyRatio;
MIX_GetMixerFrequencyRatio;
MIX_LoadAudioNoCopy;
+ MIX_LockMixer;
+ MIX_UnlockMixer;
# extra symbols go here (don't modify this line)
local: *;
};