SDL_mixer: api: Added MIX_LockMixer and MIX_UnlockMixer.

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: *;
 };