SDL_mixer: track: Don't crash in MIX_StopTrack(t, 0) if a callback destroys the track.

From 52011be7a5170796dd36ef0478d566997375c3d2 Mon Sep 17 00:00:00 2001
From: "Ryan C. Gordon" <[EMAIL REDACTED]>
Date: Mon, 18 May 2026 22:45:01 -0400
Subject: [PATCH] track: Don't crash in MIX_StopTrack(t, 0) if a callback
 destroys the track.

Fixes #852.
---
 src/SDL_mixer.c          | 12 +++++++++---
 src/SDL_mixer_internal.h |  2 +-
 2 files changed, 10 insertions(+), 4 deletions(-)

diff --git a/src/SDL_mixer.c b/src/SDL_mixer.c
index dcb5ce28..7f61c2fc 100644
--- a/src/SDL_mixer.c
+++ b/src/SDL_mixer.c
@@ -576,7 +576,7 @@ static void SDLCALL MixerCallback(void *userdata, SDL_AudioStream *stream, int a
         for (MIX_Track *track = group->tracks; track; track = next_track) {
             next_track = track->group_next;  // this won't save you from a callback going totally rogue, but it'll deal with the current track leaving the group.
 
-            track->currently_mixing = true;
+            track->currently_inuse = true;
 
             const int to_be_read = (additional_amount / SDL_AUDIO_FRAMESIZE(mixer->spec)) * SDL_AUDIO_FRAMESIZE(track->output_spec);
             const int br = SDL_GetAudioStreamData(track->output_stream, getbuf, to_be_read);
@@ -610,7 +610,7 @@ static void SDLCALL MixerCallback(void *userdata, SDL_AudioStream *stream, int a
                 }
             }
 
-            track->currently_mixing = false;
+            track->currently_inuse = false;
             if (track->destroy_requested) {  // callback asked to destroy the track while we were still using it.
                 MIX_DestroyTrack(track);  // actually kill it now.
             }
@@ -1515,7 +1515,7 @@ void MIX_DestroyTrack(MIX_Track *track)
     // handle the case where someone destroys a track during a mixer callback.  :O
     //  tracks are not currently reference-counted like MIX_Audio objects are, but
     //  we'll catch this specific case for now.
-    if (track->currently_mixing) {
+    if (track->currently_inuse) {
         track->destroy_requested = true;
         UnlockMixer(mixer);
         return;
@@ -2399,7 +2399,9 @@ static void StopTrack(MIX_Track *track, Sint64 fadeOut)
             if (track->internal_stream) {
                 SDL_ClearAudioStream(track->internal_stream);  // make sure we don't leave old data hanging around.
             }
+            track->currently_inuse = true;
             TrackStopped(track);
+            track->currently_inuse = false;
         } else {
             track->total_fade_frames = fadeOut;
             track->fade_frames = fadeOut;
@@ -2408,6 +2410,10 @@ static void StopTrack(MIX_Track *track, Sint64 fadeOut)
         }
     }
     UnlockTrack(track);
+
+    if (track->destroy_requested) {  // callback asked to destroy the track while we were still touching it.
+        MIX_DestroyTrack(track);  // actually kill it now.
+    }
 }
 
 bool MIX_StopTrack(MIX_Track *track, Sint64 fade_out_frames)
diff --git a/src/SDL_mixer_internal.h b/src/SDL_mixer_internal.h
index 0ed73cd1..bccb5b38 100644
--- a/src/SDL_mixer_internal.h
+++ b/src/SDL_mixer_internal.h
@@ -148,7 +148,7 @@ struct MIX_Track
     MIX_Audio *input_audio;    // non-NULL if used with MIX_SetTrackAudioStream. Holds a reference.
     SDL_IOStream *io;  // used for MIX_SetTrackAudio and MIX_SetTrackIOStream. Might be owned by us (SDL_IOFromConstMem of MIX_Audio::precache), or owned by the app.
     MIX_IoClamp ioclamp;  // used for MIX_SetTrackAudio and MIX_SetTrackIOStream.
-    bool currently_mixing;  // true when the mixer is running on this track.
+    bool currently_inuse;  // true when we want to delay an app's MIX_DestroyTrack request (they destroyed it from a mixer callback).
     bool destroy_requested;  // true if MIX_DestroyTrack called while the track is actively mixing.
     bool closeio;  // true if we should close `io` when changing track data.
     bool halt_when_exhausted;  // true if we should stop the track when input runs out.