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.