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);