SDL_mixer: api: Added MIX_PROP_AUDIO_LOAD_IGNORE_LOOPS_BOOLEAN. (fe30f)

From fe30f7e236ee61f3cf0e50201b8447cfa1b54070 Mon Sep 17 00:00:00 2001
From: "Ryan C. Gordon" <[EMAIL REDACTED]>
Date: Sat, 16 May 2026 19:14:22 -0400
Subject: [PATCH] api: Added MIX_PROP_AUDIO_LOAD_IGNORE_LOOPS_BOOLEAN.

This allows an app to ignore internal loop metadata when loading an audio file,
so it will play from start to end with no looping.

This is separate from a looping MIX_Track.

Fixes #847.

(cherry picked from commit 4d4e2202825f653298191ec8055ef82a7fd0d93b)
---
 include/SDL3_mixer/SDL_mixer.h | 12 +++++++++---
 src/decoder_drflac.c           |  3 ++-
 src/decoder_flac.c             |  3 ++-
 src/decoder_opus.c             |  3 ++-
 src/decoder_voc.c              |  9 +++++++++
 src/decoder_vorbis.c           |  3 ++-
 src/decoder_wav.c              |  5 ++++-
 7 files changed, 30 insertions(+), 8 deletions(-)

diff --git a/include/SDL3_mixer/SDL_mixer.h b/include/SDL3_mixer/SDL_mixer.h
index 532885eb..e00237a3 100644
--- a/include/SDL3_mixer/SDL_mixer.h
+++ b/include/SDL3_mixer/SDL_mixer.h
@@ -747,7 +747,6 @@ extern SDL_DECLSPEC MIX_Audio * SDLCALL MIX_LoadAudioNoCopy(MIX_Mixer *mixer, co
  * SDL_PropertiesID are discussed in
  * [SDL's documentation](https://wiki.libsdl.org/SDL3/CategoryProperties)
  * .
- *
  * These are the supported properties:
  *
  * - `MIX_PROP_AUDIO_LOAD_IOSTREAM_POINTER`: a pointer to an SDL_IOStream to
@@ -763,6 +762,11 @@ extern SDL_DECLSPEC MIX_Audio * SDLCALL MIX_LoadAudioNoCopy(MIX_Mixer *mixer, co
  *   metadata tags, like ID3 and APE tags. This can be used to speed up
  *   loading _if the data definitely doesn't have these tags_. Some decoders
  *   will fail if these tags are present when this property is true.
+ * - `MIX_PROP_AUDIO_LOAD_IGNORE_LOOPS_BOOLEAN`: true to ignore metadata in
+ *   the audio data specifying loop points. This will make a file decode from
+ *   start to finish without looping, even if the file specified it should
+ *   have. This audio can still be looped at playback time via MIX_Track loop
+ *   settings, regardless of this setting. Default false.
  * - `MIX_PROP_AUDIO_DECODER_STRING`: the name of the decoder to use for this
  *   data. Optional. If not specified, SDL_mixer will examine the data and
  *   choose the best decoder. These names are the same returned from
@@ -791,6 +795,7 @@ extern SDL_DECLSPEC MIX_Audio * SDLCALL MIX_LoadAudioWithProperties(SDL_Properti
 #define MIX_PROP_AUDIO_LOAD_PREDECODE_BOOLEAN "SDL_mixer.audio.load.predecode"
 #define MIX_PROP_AUDIO_LOAD_PREFERRED_MIXER_POINTER "SDL_mixer.audio.load.preferred_mixer"
 #define MIX_PROP_AUDIO_LOAD_SKIP_METADATA_TAGS_BOOLEAN "SDL_mixer.audio.load.skip_metadata_tags"
+#define MIX_PROP_AUDIO_LOAD_IGNORE_LOOPS_BOOLEAN "SDL_mixer.audio.load.ignore_loops"
 #define MIX_PROP_AUDIO_DECODER_STRING "SDL_mixer.audio.decoder"
 
 /**
@@ -3244,8 +3249,9 @@ extern SDL_DECLSPEC MIX_AudioDecoder * SDLCALL MIX_CreateAudioDecoder(const char
  *
  * This function allows properties to be specified. This is intended to supply
  * file-specific settings, such as where to find SoundFonts for a MIDI file,
- * etc. In most cases, the caller should pass a zero to specify no extra
- * properties.
+ * etc. Most of the properties available to MIX_LoadAudioWithProperties()
+ * apply here, too. In most cases, the caller should pass a zero to specify no
+ * extra properties.
  *
  * If `closeio` is true, then `io` will be closed when this decoder is done
  * with it. If this function fails and `closeio` is true, then `io` will be
diff --git a/src/decoder_drflac.c b/src/decoder_drflac.c
index 1804d7f9..40b17f8a 100644
--- a/src/decoder_drflac.c
+++ b/src/decoder_drflac.c
@@ -176,7 +176,8 @@ static bool SDLCALL DRFLAC_init_audio(SDL_IOStream *io, SDL_AudioSpec *spec, SDL
 
     adata->framesize = SDL_AUDIO_FRAMESIZE(*spec);
 
-    if (adata->loop.end > (Sint64)decoder->totalPCMFrameCount) {
+    const bool ignore_loops = SDL_GetBooleanProperty(props, MIX_PROP_AUDIO_LOAD_IGNORE_LOOPS_BOOLEAN, false);
+    if (ignore_loops || (adata->loop.end > (Sint64)decoder->totalPCMFrameCount)) {
         adata->loop.active = false;
     }
 
diff --git a/src/decoder_flac.c b/src/decoder_flac.c
index d3f3ddd5..d13cadfa 100644
--- a/src/decoder_flac.c
+++ b/src/decoder_flac.c
@@ -376,7 +376,8 @@ static bool SDLCALL FLAC_init_audio(SDL_IOStream *io, SDL_AudioSpec *spec, SDL_P
 
     SDL_copyp(spec, &tdata.spec);
 
-    if (adata->loop.end > total_frames) {
+    const bool ignore_loops = SDL_GetBooleanProperty(props, MIX_PROP_AUDIO_LOAD_IGNORE_LOOPS_BOOLEAN, false);
+    if (ignore_loops || (adata->loop.end > total_frames)) {
         adata->loop.active = false;
     }
 
diff --git a/src/decoder_opus.c b/src/decoder_opus.c
index c41c4389..4922a39a 100644
--- a/src/decoder_opus.c
+++ b/src/decoder_opus.c
@@ -179,7 +179,8 @@ static bool SDLCALL OPUS_init_audio(SDL_IOStream *io, SDL_AudioSpec *spec, SDL_P
     opus.op_raw_seek(of, 0);  // !!! FIXME: it's not clear if this seek is necessary, but https://stackoverflow.com/a/72482773 suggests it might be, at least on older libvorbisfile releases...
     const Sint64 full_length = (Sint64) opus.op_pcm_total(of, -1);
 
-    if (adata->loop.end > full_length) {
+    const bool ignore_loops = SDL_GetBooleanProperty(props, MIX_PROP_AUDIO_LOAD_IGNORE_LOOPS_BOOLEAN, false);
+    if (ignore_loops || (adata->loop.end > full_length)) {
         adata->loop.active = false;
     }
 
diff --git a/src/decoder_voc.c b/src/decoder_voc.c
index 48e095bf..4e60b61f 100644
--- a/src/decoder_voc.c
+++ b/src/decoder_voc.c
@@ -106,6 +106,7 @@ static VOC_Block *AddVocLoopBlock(VOC_AudioData *adata, int loop_count)
 // this runs during VOC_audio_init to walk the whole .VOC for metadata and sanity checks.
 static bool ParseVocFile(SDL_IOStream *io, VOC_AudioData *adata, SDL_PropertiesID props, SDL_AudioSpec *spec, Sint64 *duration_frames)
 {
+    const bool ignore_loops = SDL_GetBooleanProperty(props, MIX_PROP_AUDIO_LOAD_IGNORE_LOOPS_BOOLEAN, false);
     Sint64 total_frames = 0;
     VOC_Block *loop_start = NULL;
     int loop_start_loop_count = 0;
@@ -255,6 +256,10 @@ static bool ParseVocFile(SDL_IOStream *io, VOC_AudioData *adata, SDL_PropertiesI
             }
 
             case VOC_LOOP: {
+                if (ignore_loops) {
+                    break;  // technically this can make a file with bogus loops load where it wouldn't otherwise, but good enough.
+                }
+
                 Uint16 iterations = 0;
 
                 // !!! FIXME: can LOOP/LOOPEND sections nest? https://moddingwiki.shikadi.net/wiki/VOC_Format suggests no, saying LOOPEND goes back to _most recent_ LOOP start.
@@ -286,6 +291,10 @@ static bool ParseVocFile(SDL_IOStream *io, VOC_AudioData *adata, SDL_PropertiesI
             }
 
             case VOC_LOOPEND: {
+                if (ignore_loops) {
+                    break;  // technically this can make a file with bogus loops load where it wouldn't otherwise, but good enough.
+                }
+
                 if (!loop_start) {
                     return SDL_SetError("VOC has a LOOPEND without a matching LOOP");
                 }
diff --git a/src/decoder_vorbis.c b/src/decoder_vorbis.c
index fad3df13..9f764a89 100644
--- a/src/decoder_vorbis.c
+++ b/src/decoder_vorbis.c
@@ -205,7 +205,8 @@ static bool SDLCALL VORBIS_init_audio(SDL_IOStream *io, SDL_AudioSpec *spec, SDL
     vorbis.ov_raw_seek(&vf, 0);  // !!! FIXME: it's not clear if this seek is necessary, but https://stackoverflow.com/a/72482773 suggests it might be, at least on older libvorbisfile releases...
     const Sint64 full_length = (Sint64) vorbis.ov_pcm_total(&vf, -1);
 
-    if (adata->loop.end > full_length) {
+    const bool ignore_loops = SDL_GetBooleanProperty(props, MIX_PROP_AUDIO_LOAD_IGNORE_LOOPS_BOOLEAN, false);
+    if (ignore_loops || (adata->loop.end > full_length)) {
         adata->loop.active = false;
     }
 
diff --git a/src/decoder_wav.c b/src/decoder_wav.c
index fe1ea150..e4a41650 100644
--- a/src/decoder_wav.c
+++ b/src/decoder_wav.c
@@ -1242,6 +1242,7 @@ static bool BuildSeekBlocks(WAV_AudioData *adata)
 
 static bool WAV_init_audio_internal(WAV_AudioData *adata, SDL_IOStream *io, SDL_AudioSpec *spec, SDL_PropertiesID props)
 {
+    const bool ignore_loops = SDL_GetBooleanProperty(props, MIX_PROP_AUDIO_LOAD_IGNORE_LOOPS_BOOLEAN, false);
     Sint64 flen = SDL_GetIOSize(io);
     bool found_FMT = false;
     bool found_DATA = false;
@@ -1299,7 +1300,9 @@ static bool WAV_init_audio_internal(WAV_AudioData *adata, SDL_IOStream *io, SDL_
             chunk_okay = ParseDATA(adata, io, chunk_length);
             break;
         case SMPL:
-            chunk_okay = ParseSMPL(adata, io, chunk_length);
+            if (!ignore_loops) {
+                chunk_okay = ParseSMPL(adata, io, chunk_length);
+            }
             break;
         case LIST:
             chunk_okay = ParseLIST(adata, io, props, chunk_length);