From 7b5b902968686c6606fc82eff33bd755aee2b96a Mon Sep 17 00:00:00 2001
From: "Ryan C. Gordon" <[EMAIL REDACTED]>
Date: Tue, 20 Jan 2026 00:00:41 -0500
Subject: [PATCH] api: Added MIX_PROP_PLAY_FADE_IN_START_GAIN_FLOAT, to fade
from not-silence.
Fixes #746.
---
include/SDL3_mixer/SDL_mixer.h | 5 +++++
src/SDL_mixer.c | 13 +++++++++++--
src/SDL_mixer_internal.h | 1 +
test/testmixer.c | 1 +
4 files changed, 18 insertions(+), 2 deletions(-)
diff --git a/include/SDL3_mixer/SDL_mixer.h b/include/SDL3_mixer/SDL_mixer.h
index b6a6e34e..211a005f 100644
--- a/include/SDL3_mixer/SDL_mixer.h
+++ b/include/SDL3_mixer/SDL_mixer.h
@@ -1720,6 +1720,10 @@ extern SDL_DECLSPEC Sint64 SDLCALL MIX_FramesToMS(int sample_rate, Sint64 frames
* MIX_PROP_PLAY_FADE_IN_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_FADE_IN_START_GAIN_FLOAT`: If fading in, start fading from
+ * this volume level. 0.0f is silence and 1.0f is full volume, every in
+ * between is a linear change in gain. The specified value will be clamped
+ * between 0.0f and 1.0f. Default 0.0f.
* - `MIX_PROP_PLAY_APPEND_SILENCE_FRAMES_NUMBER`: At the end of mixing this
* track, after all loops are complete, append this many sample frames of
* silence as if it were part of the audio file. This allows for apps to
@@ -1761,6 +1765,7 @@ extern SDL_DECLSPEC bool SDLCALL MIX_PlayTrack(MIX_Track *track, SDL_PropertiesI
#define MIX_PROP_PLAY_LOOP_START_MILLISECOND_NUMBER "SDL_mixer.play.loop_start_millisecond"
#define MIX_PROP_PLAY_FADE_IN_FRAMES_NUMBER "SDL_mixer.play.fade_in_frames"
#define MIX_PROP_PLAY_FADE_IN_MILLISECONDS_NUMBER "SDL_mixer.play.fade_in_milliseconds"
+#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"
diff --git a/src/SDL_mixer.c b/src/SDL_mixer.c
index 3f4ec305..53cfdedb 100644
--- a/src/SDL_mixer.c
+++ b/src/SDL_mixer.c
@@ -253,12 +253,15 @@ static void ApplyFade(MIX_Track *track, int channels, float *pcm, int frames)
int fade_frame_position = total_fade_frames - (int) track->fade_frames;
// some hacks to avoid a branch on each sample frame. Might not be a good idea in practice.
- const float pctmult = (track->fade_direction < 0) ? 1.0f : -1.0f;
+ const float fade_start_gain = track->fade_start_gain;
+ const float pctmult = (1.0f - fade_start_gain) * ((track->fade_direction < 0) ? 1.0f : -1.0f);
const float pctsub = (track->fade_direction < 0) ? 1.0f : 0.0f;
const float ftotal_fade_frames = (float) total_fade_frames;
+ SDL_assert((fade_start_gain == 0.0f) || (track->fade_direction > 0)); // we only allow fade _in_ from arbitrary levels. Fade out always operates on the full signal down to zero.
+
for (int i = 0; i < to_be_faded; i++) {
- const float pct = (pctsub - (((float) fade_frame_position) / ftotal_fade_frames)) * pctmult;
+ const float pct = ((pctsub - (((float) fade_frame_position) / ftotal_fade_frames)) * pctmult) + fade_start_gain;
SDL_assert(pct >= 0.0f);
SDL_assert(pct <= 1.0f);
fade_frame_position++;
@@ -2177,6 +2180,7 @@ bool MIX_PlayTrack(MIX_Track *track, SDL_PropertiesID options)
Sint64 loop_start = 0;
Sint64 fade_in = 0;
Sint64 append_silence_frames = 0;
+ float fade_start_gain = 0.0f;
LockTrack(track);
if (options) {
loops = (int) SDL_GetNumberProperty(options, MIX_PROP_PLAY_LOOPS_NUMBER, loops);
@@ -2184,6 +2188,7 @@ 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);
append_silence_frames = GetTrackOptionFramesOrTicks(track, options, MIX_PROP_PLAY_APPEND_SILENCE_FRAMES_NUMBER, MIX_PROP_PLAY_APPEND_SILENCE_MILLISECONDS_NUMBER, append_silence_frames);
if (start_pos < 0) {
@@ -2197,6 +2202,8 @@ bool MIX_PlayTrack(MIX_Track *track, SDL_PropertiesID options)
if (append_silence_frames < 0) {
append_silence_frames = 0;
}
+
+ fade_start_gain = SDL_clamp(fade_start_gain, 0.0f, 1.0f);
}
if (track->input_audio && (!track->input_audio->decoder->seek(track->decoder_userdata, start_pos))) {
@@ -2213,6 +2220,7 @@ bool MIX_PlayTrack(MIX_Track *track, SDL_PropertiesID options)
track->total_fade_frames = (fade_in > 0) ? fade_in : 0;
track->fade_frames = track->total_fade_frames;
track->fade_direction = (fade_in > 0) ? 1 : 0;
+ track->fade_start_gain = fade_start_gain;
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;
@@ -2303,6 +2311,7 @@ static void StopTrack(MIX_Track *track, Sint64 fadeOut)
track->total_fade_frames = fadeOut;
track->fade_frames = fadeOut;
track->fade_direction = -1;
+ track->fade_start_gain = 0.0f; // only used for fade-ins.
}
}
UnlockTrack(track);
diff --git a/src/SDL_mixer_internal.h b/src/SDL_mixer_internal.h
index be9aecda..3e0e514a 100644
--- a/src/SDL_mixer_internal.h
+++ b/src/SDL_mixer_internal.h
@@ -161,6 +161,7 @@ struct MIX_Track
Sint64 total_fade_frames; // fade in or out for this many sample frames.
Sint64 fade_frames; // remaining frames to fade.
int fade_direction; // -1: fade out 0: don't fade 1: fade in
+ float fade_start_gain; // between 0.0f and 1.0f. Fade with this volume as the starting point (fade-in only).
int loops_remaining; // seek to loop_start and continue this many more times at end of input. Negative to loop forever.
int loop_start; // sample frame position for loops to begin, so you can play an intro once and then loop from an internal point thereafter.
SDL_PropertiesID tags; // lookup tags to see if they are currently applied to this track (true or false).
diff --git a/test/testmixer.c b/test/testmixer.c
index 0449ae79..b2bc4360 100644
--- a/test/testmixer.c
+++ b/test/testmixer.c
@@ -234,6 +234,7 @@ SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
SDL_SetNumberProperty(options, MIX_PROP_PLAY_LOOPS_NUMBER, 3);
SDL_SetNumberProperty(options, MIX_PROP_PLAY_LOOP_START_MILLISECOND_NUMBER, 6097);
SDL_SetNumberProperty(options, MIX_PROP_PLAY_FADE_IN_MILLISECONDS_NUMBER, 30000);
+ //SDL_SetFloatProperty(options, MIX_PROP_PLAY_FADE_IN_START_GAIN_FLOAT, 0.25f);
SDL_SetNumberProperty(options, MIX_PROP_PLAY_APPEND_SILENCE_MILLISECONDS_NUMBER, 30000);
MIX_PlayTrack(track1, options);
SDL_DestroyProperties(options);