SDL_mixer: api: Added MIX_PROP_PLAY_HALT_WHEN_EXHAUSTED_BOOLEAN.

From 986659f4ab12c78a290284a65c2c1657f5fc00b0 Mon Sep 17 00:00:00 2001
From: "Ryan C. Gordon" <[EMAIL REDACTED]>
Date: Thu, 5 Mar 2026 00:06:54 -0500
Subject: [PATCH] api: Added MIX_PROP_PLAY_HALT_WHEN_EXHAUSTED_BOOLEAN.

Fixes #816.
---
 include/SDL3_mixer/SDL_mixer.h | 20 +++++++++++++++++++-
 src/SDL_mixer.c                | 14 +++++++++++---
 src/SDL_mixer_internal.h       |  1 +
 3 files changed, 31 insertions(+), 4 deletions(-)

diff --git a/include/SDL3_mixer/SDL_mixer.h b/include/SDL3_mixer/SDL_mixer.h
index fa43e656..835f4242 100644
--- a/include/SDL3_mixer/SDL_mixer.h
+++ b/include/SDL3_mixer/SDL_mixer.h
@@ -1798,6 +1798,24 @@ extern SDL_DECLSPEC Sint64 SDLCALL MIX_FramesToMS(int sample_rate, Sint64 frames
  *   MIX_PROP_PLAY_APPEND_SILENCE_FRAMES_NUMBER property, but the value is
  *   specified in milliseconds instead of sample frames. If both properties
  *   are specified, the sample frames value is favored. Default 0.
+ * - `MIX_PROP_PLAY_HALT_WHEN_EXHAUSTED_BOOLEAN`: If true, when input is
+ *   completely consumed for the track, the mixer will mark the track as
+ *   stopped (and call any appropriate MIX_TrackStoppedCallback, etc); to play
+ *   more, the track will need to be restarted. If false, the track will just
+ *   not contribute to the mix, but it will not be marked as stopped. There
+ *   may be clever logic tricks this exposes generally, but this property is
+ *   specifically useful when the track's input is an SDL_AudioStream assigned
+ *   via MIX_SetTrackAudioStream(). Setting this property to true can be
+ *   useful when pushing a complete piece of audio to the stream that has a
+ *   definite ending, as the track will operate like any other audio was
+ *   applied. Setting to false means as new data is added to the stream, the
+ *   mixer will start using it as soon as possible, which is useful when audio
+ *   should play immediately as it drips in: new VoIP packets, etc. Note that
+ *   in this situation, if the audio runs out when needed, there _will_ be gaps
+ *   in the mixed output, so try to buffer enough data to avoid this when
+ *   possible. Note that a track is not consider exhausted until all its loops
+ *   and appended silence have been mixed (and also, that loops don't mean
+ *   anything when the input is an AudioStream). Default true.
  *
  * If this function fails, mixing of this track will not start (or restart, if
  * it was already started).
@@ -1830,7 +1848,7 @@ extern SDL_DECLSPEC bool SDLCALL MIX_PlayTrack(MIX_Track *track, SDL_PropertiesI
 #define MIX_PROP_PLAY_FADE_IN_START_GAIN_FLOAT "SDL_mixer.play.fade_in_start_gain"
 #define MIX_PROP_PLAY_APPEND_SILENCE_FRAMES_NUMBER "SDL_mixer.play.append_silence_frames"
 #define MIX_PROP_PLAY_APPEND_SILENCE_MILLISECONDS_NUMBER "SDL_mixer.play.append_silence_milliseconds"
-
+#define MIX_PROP_PLAY_HALT_WHEN_EXHAUSTED_BOOLEAN "SDL_mixer.play.halt_when_exhausted"
 
 /**
  * Start (or restart) mixing all tracks with a specific tag for playback.
diff --git a/src/SDL_mixer.c b/src/SDL_mixer.c
index 3de3a47b..fc387de6 100644
--- a/src/SDL_mixer.c
+++ b/src/SDL_mixer.c
@@ -462,7 +462,11 @@ static void SDLCALL TrackGetCallback(void *userdata, SDL_AudioStream *stream, in
             }
 
             if (track_stopped) {
-                TrackStopped(track);
+                if (track->halt_when_exhausted) {
+                    TrackStopped(track);
+                } else {
+                    break;  // done with this track for now, but don't halt the track. We'll try it again later.
+                }
             }
         }
     }
@@ -1421,6 +1425,7 @@ MIX_Track *MIX_CreateTrack(MIX_Mixer *mixer)
     SDL_SetAudioStreamGetCallback(track->output_stream, TrackGetCallback, track);
 
     track->mixer = mixer;
+    track->halt_when_exhausted = true;
 
     LockMixer(mixer);
     track->next = mixer->all_tracks;
@@ -2224,6 +2229,8 @@ bool MIX_PlayTrack(MIX_Track *track, SDL_PropertiesID options)
     Sint64 fade_in = 0;
     Sint64 append_silence_frames = 0;
     float fade_start_gain = 0.0f;
+    bool halt_when_exhausted = true;
+
     LockTrack(track);
     if (options) {
         loops = (int) SDL_GetNumberProperty(options, MIX_PROP_PLAY_LOOPS_NUMBER, loops);
@@ -2231,8 +2238,9 @@ bool MIX_PlayTrack(MIX_Track *track, SDL_PropertiesID options)
         start_pos = GetTrackOptionFramesOrTicks(track, options, MIX_PROP_PLAY_START_FRAME_NUMBER, MIX_PROP_PLAY_START_MILLISECOND_NUMBER, start_pos);
         loop_start = GetTrackOptionFramesOrTicks(track, options, MIX_PROP_PLAY_LOOP_START_FRAME_NUMBER, MIX_PROP_PLAY_LOOP_START_MILLISECOND_NUMBER, loop_start);
         fade_in = GetTrackOptionFramesOrTicks(track, options, MIX_PROP_PLAY_FADE_IN_FRAMES_NUMBER, MIX_PROP_PLAY_FADE_IN_MILLISECONDS_NUMBER, fade_in);
-        fade_start_gain = SDL_GetFloatProperty(options, MIX_PROP_PLAY_FADE_IN_START_GAIN_FLOAT, 0.0f);
+        fade_start_gain = SDL_GetFloatProperty(options, MIX_PROP_PLAY_FADE_IN_START_GAIN_FLOAT, fade_start_gain);
         append_silence_frames = GetTrackOptionFramesOrTicks(track, options, MIX_PROP_PLAY_APPEND_SILENCE_FRAMES_NUMBER, MIX_PROP_PLAY_APPEND_SILENCE_MILLISECONDS_NUMBER, append_silence_frames);
+        halt_when_exhausted = SDL_GetBooleanProperty(options, MIX_PROP_PLAY_HALT_WHEN_EXHAUSTED_BOOLEAN, halt_when_exhausted);
 
         if (start_pos < 0) {
             start_pos = 0;
@@ -2267,6 +2275,7 @@ bool MIX_PlayTrack(MIX_Track *track, SDL_PropertiesID options)
     track->silence_frames = (append_silence_frames > 0) ? -append_silence_frames : 0;  // negative means "there is still actual audio data to play", positive means "we're done with actual data, feed silence now." Zero means no silence (left) to feed.
     track->state = MIX_STATE_PLAYING;
     track->position = start_pos;
+    track->halt_when_exhausted = halt_when_exhausted;
 
     UnlockTrack(track);
     return true;
@@ -2770,7 +2779,6 @@ bool MIX_SetTrackStereo(MIX_Track *track, const MIX_StereoGains *gains)
 
 }
 
-
 bool MIX_SetTrack3DPosition(MIX_Track *track, const MIX_Point3D *position)
 {
     if (!CheckTrackParam(track)) {
diff --git a/src/SDL_mixer_internal.h b/src/SDL_mixer_internal.h
index cdc0c45b..dd00758c 100644
--- a/src/SDL_mixer_internal.h
+++ b/src/SDL_mixer_internal.h
@@ -148,6 +148,7 @@ struct MIX_Track
     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 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.
     SDL_AudioStream *input_stream;  // used for both MIX_SetTrackAudio and MIX_SetTrackAudioStream. Maybe not owned by SDL_mixer!
     SDL_AudioStream *internal_stream;  // used with MIX_SetTrackAudio, where it is also assigned to input_stream. Owned by SDL_mixer!
     void *decoder_userdata;  // MIX_Decoder-specific data for this run, if any.