SDL_mixer: mod: Allow mods to start at specific "order". (54cc6)

From 54cc67fea050da12b8be7faf69a91e3244824e49 Mon Sep 17 00:00:00 2001
From: "Ryan C. Gordon" <[EMAIL REDACTED]>
Date: Tue, 10 Mar 2026 17:47:22 -0400
Subject: [PATCH] mod: Allow mods to start at specific "order".

Fixes #826.

(cherry picked from commit b00487513231592c2c124e11cece9f561e7b6856)
---
 docs/README-migration.md       | 12 +++++-------
 include/SDL3_mixer/SDL_mixer.h |  9 +++++++++
 src/SDL_mixer.c                | 11 ++++++++++-
 src/SDL_mixer_internal.h       |  1 +
 src/decoder_aiff.c             |  1 +
 src/decoder_au.c               |  1 +
 src/decoder_drflac.c           |  1 +
 src/decoder_drmp3.c            |  1 +
 src/decoder_flac.c             |  1 +
 src/decoder_fluidsynth.c       |  1 +
 src/decoder_gme.c              |  1 +
 src/decoder_mpg123.c           |  1 +
 src/decoder_opus.c             |  1 +
 src/decoder_raw.c              |  1 +
 src/decoder_sinewave.c         |  1 +
 src/decoder_stb_vorbis.c       |  1 +
 src/decoder_timidity.c         |  1 +
 src/decoder_voc.c              |  1 +
 src/decoder_vorbis.c           |  1 +
 src/decoder_wav.c              |  1 +
 src/decoder_wavpack.c          |  1 +
 src/decoder_xmp.c              |  8 ++++++++
 22 files changed, 50 insertions(+), 8 deletions(-)

diff --git a/docs/README-migration.md b/docs/README-migration.md
index 1c4a253b..15aee045 100644
--- a/docs/README-migration.md
+++ b/docs/README-migration.md
@@ -17,8 +17,8 @@ think that once you move to them, you'll be quite happy you did.
 There are a lot of things that don't have simple replacements that can be
 changed mechanically to migrate to SDL3_mixer. The new API is in many ways
 more powerful, but also much simpler. For example, there's no equivalent of
-Mix_ModMusicJumpToOrder(), because messing with the specifics
-of MOD files in the public API is both uncommon and generally pretty messy.
+Mix_GetNumTracks(), because messing with the specifics of MOD files in the
+public API is both uncommon and generally pretty messy.
 
 This migration guide will attempt to walk through the important details but
 it's possible that some things can't be done the same way. Feel free to open
@@ -34,10 +34,8 @@ function name and a brief explanation about what to do with it.
   and packaged it as a separate library that can be used alongside SDL3_mixer,
   or without SDL_mixer at all: https://github.com/libsdl-org/SDL_native_midi
 
-- Mix_GetNumTracks(), Mix_StartTrack(), and Mix_ModMusicJumpToOrder() have
-  been removed; these were decoder-specific APIs.
-
-- Mix_SetSoundFonts(), Mix_GetSoundFonts(), Mix_EachSoundFont(),
+- Mix_GetNumTracks(), Mix_StartTrack(), Mix_ModMusicJumpToOrder(),
+  Mix_SetSoundFonts(), Mix_GetSoundFonts(), Mix_EachSoundFont(),
   Mix_SetTimidityCfg(), Mix_GetTimidityCfg(): these have been removed, but
   decoder-specific settings can be passed on in a generic way in SDL3_mixer,
   using SDL properties.
@@ -392,7 +390,7 @@ can discuss it!
 - Mix_ResumeMusic => MIX_ResumeTrack
 - Mix_RewindMusic => MIX_SetTrackPlaybackPosition(track, 0)
 - Mix_PausedMusic => MIX_TrackPaused
-- Mix_ModMusicJumpToOrder => no equivalent in SDL3_mixer.
+- Mix_ModMusicJumpToOrder => MIX_PlayTrack with MIX_PROP_PLAY_START_ORDER_NUMBER property set.
 - Mix_StartTrack => no equivalent in SDL3_mixer.
 - Mix_GetNumTracks => no equivalent in SDL3_mixer.
 - Mix_SetMusicPosition => MIX_SetTrackPlaybackPosition
diff --git a/include/SDL3_mixer/SDL_mixer.h b/include/SDL3_mixer/SDL_mixer.h
index 926da8f9..5d6b770a 100644
--- a/include/SDL3_mixer/SDL_mixer.h
+++ b/include/SDL3_mixer/SDL_mixer.h
@@ -1885,6 +1885,14 @@ extern SDL_DECLSPEC Sint64 SDLCALL MIX_FramesToMS(int sample_rate, Sint64 frames
  *   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.
+ * - `MIX_PROP_PLAY_START_ORDER_NUMBER`: This is a special-case property
+ *   that most apps can ignore. For mod file formats, start mixing from a
+ *   specific "order" index instead of the start of the file. A value < 0 will
+ *   cause this property to be ignored. If the decoder doesn't support this
+ *   property, it will also be ignored. If this property is _not_ ignored,
+ *   the MIX_PROP_PLAY_START_FRAME_NUMBER and
+ *   MIX_PROP_PLAY_START_MILLISECOND_NUMBER properties will be ignored instead.
+ *   Default -1. Since SDL_mixer 3.2.2.
  *
  * If this function fails, mixing of this track will not start (or restart, if
  * it was already started).
@@ -1910,6 +1918,7 @@ extern SDL_DECLSPEC bool SDLCALL MIX_PlayTrack(MIX_Track *track, SDL_PropertiesI
 #define MIX_PROP_PLAY_MAX_MILLISECONDS_NUMBER "SDL_mixer.play.max_milliseconds"
 #define MIX_PROP_PLAY_START_FRAME_NUMBER "SDL_mixer.play.start_frame"
 #define MIX_PROP_PLAY_START_MILLISECOND_NUMBER "SDL_mixer.play.start_millisecond"
+#define MIX_PROP_PLAY_START_ORDER_NUMBER "SDL_mixer.play.start_order"
 #define MIX_PROP_PLAY_LOOP_START_FRAME_NUMBER "SDL_mixer.play.loop_start_frame"
 #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"
diff --git a/src/SDL_mixer.c b/src/SDL_mixer.c
index 4630e5f1..e9b16ef7 100644
--- a/src/SDL_mixer.c
+++ b/src/SDL_mixer.c
@@ -2242,6 +2242,7 @@ bool MIX_PlayTrack(MIX_Track *track, SDL_PropertiesID options)
     Sint64 loop_start = 0;
     Sint64 fade_in = 0;
     Sint64 append_silence_frames = 0;
+    int start_order = -1;
     float fade_start_gain = 0.0f;
     bool halt_when_exhausted = true;
 
@@ -2255,6 +2256,7 @@ bool MIX_PlayTrack(MIX_Track *track, SDL_PropertiesID options)
         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);
+        start_order = (int) SDL_GetNumberProperty(options, MIX_PROP_PLAY_START_ORDER_NUMBER, start_order);
 
         if (start_pos < 0) {
             start_pos = 0;
@@ -2271,7 +2273,14 @@ bool MIX_PlayTrack(MIX_Track *track, SDL_PropertiesID options)
         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))) {
+    if ((start_order >= 0) && (!track->input_audio || !track->input_audio->decoder->jump_to_order)) {
+        start_order = -1;  // ignore this option, it doesn't mean anything on this decoder.
+    }
+
+    if ((start_order >= 0) && !track->input_audio->decoder->jump_to_order(track->decoder_userdata, start_order)) {
+        UnlockTrack(track);
+        return false;
+    } else if (track->input_audio && (!track->input_audio->decoder->seek(track->decoder_userdata, start_pos))) {
         UnlockTrack(track);
         return false;
     } else if (!track->input_audio && (start_pos != 0)) {
diff --git a/src/SDL_mixer_internal.h b/src/SDL_mixer_internal.h
index dd00758c..906b47f4 100644
--- a/src/SDL_mixer_internal.h
+++ b/src/SDL_mixer_internal.h
@@ -105,6 +105,7 @@ typedef struct MIX_Decoder
     bool (SDLCALL *init_track)(void *audio_userdata, SDL_IOStream *io, const SDL_AudioSpec *spec, SDL_PropertiesID props, void **track_userdata);  // init decoder instance data for a single track.
     bool (SDLCALL *decode)(void *track_userdata, SDL_AudioStream *stream);
     bool (SDLCALL *seek)(void *track_userdata, Uint64 frame);
+    bool (SDLCALL *jump_to_order)(void *track_userdata, int order);
     void (SDLCALL *quit_track)(void *track_userdata);
     void (SDLCALL *quit_audio)(void *audio_userdata);
     void (SDLCALL *quit)(void);   // deinitialize the decoder (unload external libraries, etc).
diff --git a/src/decoder_aiff.c b/src/decoder_aiff.c
index 3ffac607..8aefd9d7 100644
--- a/src/decoder_aiff.c
+++ b/src/decoder_aiff.c
@@ -581,6 +581,7 @@ const MIX_Decoder MIX_Decoder_AIFF = {
     AIFF_init_track,
     AIFF_decode,
     AIFF_seek,
+    NULL,  // jump_to_order
     AIFF_quit_track,
     AIFF_quit_audio,
     NULL  // quit
diff --git a/src/decoder_au.c b/src/decoder_au.c
index 1705cd04..04fb1249 100644
--- a/src/decoder_au.c
+++ b/src/decoder_au.c
@@ -269,6 +269,7 @@ const MIX_Decoder MIX_Decoder_AU = {
     AU_init_track,
     AU_decode,
     AU_seek,
+    NULL,  // jump_to_order
     AU_quit_track,
     AU_quit_audio,
     NULL  // quit
diff --git a/src/decoder_drflac.c b/src/decoder_drflac.c
index 292e4420..1804d7f9 100644
--- a/src/decoder_drflac.c
+++ b/src/decoder_drflac.c
@@ -331,6 +331,7 @@ const MIX_Decoder MIX_Decoder_DRFLAC = {
     DRFLAC_init_track,
     DRFLAC_decode,
     DRFLAC_seek,
+    NULL,  // jump_to_order
     DRFLAC_quit_track,
     DRFLAC_quit_audio,
     NULL  // quit
diff --git a/src/decoder_drmp3.c b/src/decoder_drmp3.c
index 388fde4a..e4767799 100644
--- a/src/decoder_drmp3.c
+++ b/src/decoder_drmp3.c
@@ -195,6 +195,7 @@ const MIX_Decoder MIX_Decoder_DRMP3 = {
     DRMP3_init_track,
     DRMP3_decode,
     DRMP3_seek,
+    NULL,  // jump_to_order
     DRMP3_quit_track,
     DRMP3_quit_audio,
     NULL  // quit
diff --git a/src/decoder_flac.c b/src/decoder_flac.c
index d6ef83fb..d3f3ddd5 100644
--- a/src/decoder_flac.c
+++ b/src/decoder_flac.c
@@ -507,6 +507,7 @@ const MIX_Decoder MIX_Decoder_FLAC = {
     FLAC_init_track,
     FLAC_decode,
     FLAC_seek,
+    NULL,  // jump_to_order
     FLAC_quit_track,
     FLAC_quit_audio,
     FLAC_quit
diff --git a/src/decoder_fluidsynth.c b/src/decoder_fluidsynth.c
index 06772a2c..bd385f2b 100644
--- a/src/decoder_fluidsynth.c
+++ b/src/decoder_fluidsynth.c
@@ -451,6 +451,7 @@ const MIX_Decoder MIX_Decoder_FLUIDSYNTH = {
     FLUIDSYNTH_init_track,
     FLUIDSYNTH_decode,
     FLUIDSYNTH_seek,
+    NULL,  // jump_to_order
     FLUIDSYNTH_quit_track,
     FLUIDSYNTH_quit_audio,
     FLUIDSYNTH_quit
diff --git a/src/decoder_gme.c b/src/decoder_gme.c
index 07445dca..4d84d9f8 100644
--- a/src/decoder_gme.c
+++ b/src/decoder_gme.c
@@ -222,6 +222,7 @@ const MIX_Decoder MIX_Decoder_GME = {
     GME_init_track,
     GME_decode,
     GME_seek,
+    NULL,  // jump_to_order
     GME_quit_track,
     GME_quit_audio,
     GME_quit
diff --git a/src/decoder_mpg123.c b/src/decoder_mpg123.c
index 47da15bc..7b09eb66 100644
--- a/src/decoder_mpg123.c
+++ b/src/decoder_mpg123.c
@@ -459,6 +459,7 @@ const MIX_Decoder MIX_Decoder_MPG123 = {
     MPG123_init_track,
     MPG123_decode,
     MPG123_seek,
+    NULL,  // jump_to_order
     MPG123_quit_track,
     MPG123_quit_audio,
     MPG123_quit
diff --git a/src/decoder_opus.c b/src/decoder_opus.c
index d38ffd48..c41c4389 100644
--- a/src/decoder_opus.c
+++ b/src/decoder_opus.c
@@ -357,6 +357,7 @@ const MIX_Decoder MIX_Decoder_OPUS = {
     OPUS_init_track,
     OPUS_decode,
     OPUS_seek,
+    NULL,  // jump_to_order
     OPUS_quit_track,
     OPUS_quit_audio,
     OPUS_quit
diff --git a/src/decoder_raw.c b/src/decoder_raw.c
index f5ce85b5..8e84d1a1 100644
--- a/src/decoder_raw.c
+++ b/src/decoder_raw.c
@@ -137,6 +137,7 @@ const MIX_Decoder MIX_Decoder_RAW = {
     RAW_init_track,
     RAW_decode,
     RAW_seek,
+    NULL,  // jump_to_order
     RAW_quit_track,
     RAW_quit_audio,
     NULL  // quit
diff --git a/src/decoder_sinewave.c b/src/decoder_sinewave.c
index b43aab60..c301663c 100644
--- a/src/decoder_sinewave.c
+++ b/src/decoder_sinewave.c
@@ -160,6 +160,7 @@ const MIX_Decoder MIX_Decoder_SINEWAVE = {
     SINEWAVE_init_track,
     SINEWAVE_decode,
     SINEWAVE_seek,
+    NULL,  // jump_to_order
     SINEWAVE_quit_track,
     SINEWAVE_quit_audio,
     NULL  // quit
diff --git a/src/decoder_stb_vorbis.c b/src/decoder_stb_vorbis.c
index e7115e18..b69a7794 100644
--- a/src/decoder_stb_vorbis.c
+++ b/src/decoder_stb_vorbis.c
@@ -349,6 +349,7 @@ const MIX_Decoder MIX_Decoder_STBVORBIS = {
     STBVORBIS_init_track,
     STBVORBIS_decode,
     STBVORBIS_seek,
+    NULL,  // jump_to_order
     STBVORBIS_quit_track,
     STBVORBIS_quit_audio,
     STBVORBIS_quit
diff --git a/src/decoder_timidity.c b/src/decoder_timidity.c
index 2f95aed0..54f4cff3 100644
--- a/src/decoder_timidity.c
+++ b/src/decoder_timidity.c
@@ -169,6 +169,7 @@ const MIX_Decoder MIX_Decoder_TIMIDITY = {
     TIMIDITY_init_track,
     TIMIDITY_decode,
     TIMIDITY_seek,
+    NULL,  // jump_to_order
     TIMIDITY_quit_track,
     TIMIDITY_quit_audio,
     TIMIDITY_quit
diff --git a/src/decoder_voc.c b/src/decoder_voc.c
index 085c2ce7..48e095bf 100644
--- a/src/decoder_voc.c
+++ b/src/decoder_voc.c
@@ -590,6 +590,7 @@ const MIX_Decoder MIX_Decoder_VOC = {
     VOC_init_track,
     VOC_decode,
     VOC_seek,
+    NULL,  // jump_to_order
     VOC_quit_track,
     VOC_quit_audio,
     NULL  // quit
diff --git a/src/decoder_vorbis.c b/src/decoder_vorbis.c
index f4dcc180..fad3df13 100644
--- a/src/decoder_vorbis.c
+++ b/src/decoder_vorbis.c
@@ -439,6 +439,7 @@ const MIX_Decoder MIX_Decoder_VORBIS = {
     VORBIS_init_track,
     VORBIS_decode,
     VORBIS_seek,
+    NULL,  // jump_to_order
     VORBIS_quit_track,
     VORBIS_quit_audio,
     VORBIS_quit
diff --git a/src/decoder_wav.c b/src/decoder_wav.c
index 52fe415f..fe1ea150 100644
--- a/src/decoder_wav.c
+++ b/src/decoder_wav.c
@@ -1785,6 +1785,7 @@ const MIX_Decoder MIX_Decoder_WAV = {
     WAV_init_track,
     WAV_decode,
     WAV_seek,
+    NULL,  // jump_to_order
     WAV_quit_track,
     WAV_quit_audio,
     NULL  // quit
diff --git a/src/decoder_wavpack.c b/src/decoder_wavpack.c
index c664b4ad..0a20141f 100644
--- a/src/decoder_wavpack.c
+++ b/src/decoder_wavpack.c
@@ -633,6 +633,7 @@ const MIX_Decoder MIX_Decoder_WAVPACK = {
     WAVPACK_init_track,
     WAVPACK_decode,
     WAVPACK_seek,
+    NULL,  // jump_to_order
     WAVPACK_quit_track,
     WAVPACK_quit_audio,
     WAVPACK_quit
diff --git a/src/decoder_xmp.c b/src/decoder_xmp.c
index 334c812d..6282e47c 100644
--- a/src/decoder_xmp.c
+++ b/src/decoder_xmp.c
@@ -65,6 +65,7 @@ struct xmp_callbacks {
     MIX_LOADER_FUNCTION(true,int,xmp_play_frame,(xmp_context)) \
     MIX_LOADER_FUNCTION(true,int,xmp_play_buffer,(xmp_context, void *, int, int)) \
     MIX_LOADER_FUNCTION(true,int,xmp_seek_time,(xmp_context, int)) \
+    MIX_LOADER_FUNCTION(true,int,xmp_set_position,(xmp_context, int)) \
     MIX_LOADER_FUNCTION(true,void,xmp_get_frame_info,(xmp_context, struct xmp_frame_info *)) \
     MIX_LOADER_FUNCTION(true,void,xmp_stop_module,(xmp_context)) \
     MIX_LOADER_FUNCTION(true,void,xmp_release_module,(xmp_context)) \
@@ -278,6 +279,12 @@ static bool SDLCALL XMP_seek(void *track_userdata, Uint64 frame)
     return err < 0 ? SetLibXmpError("xmp_seek_time", err) : true;
 }
 
+static bool SDLCALL XMP_jump_to_order(void *track_userdata, int order)
+{
+    XMP_TrackData *tdata = (XMP_TrackData *) track_userdata;
+    return libxmp.xmp_set_position(tdata->ctx, order);
+}
+
 static void SDLCALL XMP_quit_track(void *track_userdata)
 {
     XMP_TrackData *tdata = (XMP_TrackData *) track_userdata;
@@ -300,6 +307,7 @@ const MIX_Decoder MIX_Decoder_XMP = {
     XMP_init_track,
     XMP_decode,
     XMP_seek,
+    XMP_jump_to_order,
     XMP_quit_track,
     XMP_quit_audio,
     XMP_quit