From e21e494eac6fe800edfa8b7101363694ebd449d8 Mon Sep 17 00:00:00 2001
From: "Ryan C. Gordon" <[EMAIL REDACTED]>
Date: Mon, 29 Dec 2025 16:38:10 -0500
Subject: [PATCH] api: Added MIX_GetTrackTags().
Reference Issue #759.
---
include/SDL3_mixer/SDL_mixer.h | 19 +++++++++
src/SDL_mixer.c | 74 ++++++++++++++++++++++++++++++++++
src/SDL_mixer.exports | 1 +
src/SDL_mixer.sym | 1 +
test/testmixer.c | 38 +++++++++++++++++
5 files changed, 133 insertions(+)
diff --git a/include/SDL3_mixer/SDL_mixer.h b/include/SDL3_mixer/SDL_mixer.h
index 0acab79f1..868e0befe 100644
--- a/include/SDL3_mixer/SDL_mixer.h
+++ b/include/SDL3_mixer/SDL_mixer.h
@@ -1257,6 +1257,25 @@ extern SDL_DECLSPEC bool SDLCALL MIX_TagTrack(MIX_Track *track, const char *tag)
*/
extern SDL_DECLSPEC void SDLCALL MIX_UntagTrack(MIX_Track *track, const char *tag);
+/**
+ * Get the tags currently associated with a track.
+ *
+ * Tags are not provided in any guaranteed order.
+ *
+ * \param track the track to query.
+ * \param count a pointer filled in with the number of tags returned, can
+ * be NULL.
+ * \returns an array of the tags, NULL-terminated, or NULL on
+ * failure; call SDL_GetError() for more information. This is a
+ * single allocation that should be freed with SDL_free() when it is
+ * no longer needed.
+ *
+ * \threadsafety It is safe to call this function from any thread.
+ *
+ * \since This function is available since SDL_mixer 3.0.0.
+ */
+extern SDL_DECLSPEC char ** SDLCALL MIX_GetTrackTags(MIX_Track *track, int *count);
+
/**
* Seek a playing track to a new position in its input.
*
diff --git a/src/SDL_mixer.c b/src/SDL_mixer.c
index 637bc03a4..f119a7b3f 100644
--- a/src/SDL_mixer.c
+++ b/src/SDL_mixer.c
@@ -1844,6 +1844,80 @@ void MIX_UntagTrack(MIX_Track *track, const char *tag)
}
}
+typedef struct GetTrackTagsCallbackData
+{
+ const char *struct_tags[4]; // hopefully mostly fits in here, no allocations.
+ const char **allocated_tags;
+ int count;
+ bool failed;
+} GetTrackTagsCallbackData;
+
+static void SDLCALL GetTrackTagsCallback(void *userdata, SDL_PropertiesID props, const char *tag)
+{
+ // just store the tag to the array; since we have the properties locked, we can copy it after enumeration is done.
+ GetTrackTagsCallbackData *data = (GetTrackTagsCallbackData *) userdata;
+ if (data->failed) {
+ return; // just get out if we previously failed.
+ } else if (SDL_GetBooleanProperty(props, tag, false)) { // if false, tag _was_ here, but has since been untagged. Skip it.
+ if (data->count < SDL_arraysize(data->struct_tags)) {
+ data->struct_tags[data->count++] = tag;
+ } else {
+ void *ptr = SDL_realloc(data->allocated_tags, sizeof (char *) * (data->count - SDL_arraysize(data->struct_tags) + 1));
+ if (!ptr) {
+ data->failed = true;
+ } else {
+ data->allocated_tags = (const char **) ptr;
+ data->allocated_tags[data->count - SDL_arraysize(data->struct_tags)] = tag;
+ data->count++;
+ }
+ }
+ }
+}
+
+char **MIX_GetTrackTags(MIX_Track *track, int *count)
+{
+ char **retval = NULL;
+ int dummycount;
+ if (!count) {
+ count = &dummycount;
+ }
+ *count = 0;
+
+ if (!CheckTrackParam(track)) {
+ return NULL;
+ }
+
+ GetTrackTagsCallbackData data;
+ SDL_zero(data);
+ SDL_LockProperties(track->tags);
+ SDL_EnumerateProperties(track->tags, GetTrackTagsCallback, &data);
+ if (!data.failed) {
+ size_t allocation = sizeof (char *); // one extra pointer for the list's NULL terminator.
+ for (int i = 0; i < data.count; i++) {
+ const char *str = (i < SDL_arraysize(data.struct_tags)) ? data.struct_tags[i] : data.allocated_tags[i - SDL_arraysize(data.struct_tags)];
+ allocation += sizeof (char *) + SDL_strlen(str) + 1;
+ }
+ retval = (char **) SDL_malloc(allocation);
+ if (retval) {
+ char *strptr = ((char *) retval) + (sizeof (char *) * (data.count + 1));
+ for (int i = 0; i < data.count; i++) {
+ const char *str = (i < SDL_arraysize(data.struct_tags)) ? data.struct_tags[i] : data.allocated_tags[i - SDL_arraysize(data.struct_tags)];
+ const size_t slen = SDL_strlen(str) + 1;
+ SDL_memcpy(strptr, str, slen);
+ retval[i] = strptr;
+ strptr += slen;
+ }
+ retval[data.count] = NULL;
+ *count = data.count;
+ }
+ SDL_free(data.allocated_tags);
+ }
+ SDL_UnlockProperties(track->tags);
+
+ return retval;
+}
+
+
bool MIX_SetTrackPlaybackPosition(MIX_Track *track, Sint64 frames)
{
if (!CheckTrackParam(track)) {
diff --git a/src/SDL_mixer.exports b/src/SDL_mixer.exports
index 3e530c112..2f990da1b 100644
--- a/src/SDL_mixer.exports
+++ b/src/SDL_mixer.exports
@@ -86,4 +86,5 @@ _MIX_DecodeAudio
_MIX_GetAudioDecoderFormat
_MIX_SetTrackLoops
_MIX_GetTrackFadeFrames
+_MIX_GetTrackTags
# extra symbols go here (don't modify this line)
diff --git a/src/SDL_mixer.sym b/src/SDL_mixer.sym
index be386d384..51ccaabfb 100644
--- a/src/SDL_mixer.sym
+++ b/src/SDL_mixer.sym
@@ -87,6 +87,7 @@ SDL3_mixer_0.0.0 {
MIX_GetAudioDecoderFormat;
MIX_SetTrackLoops;
MIX_GetTrackFadeFrames;
+ MIX_GetTrackTags;
# extra symbols go here (don't modify this line)
local: *;
};
diff --git a/test/testmixer.c b/test/testmixer.c
index c0e206d06..0f47ef794 100644
--- a/test/testmixer.c
+++ b/test/testmixer.c
@@ -4,6 +4,7 @@
#include "SDL3_mixer/SDL_mixer.h"
#define USE_MIX_GENERATE 0
+#define TEST_TAGS 0
//static SDL_Window *window = NULL;
//static SDL_Renderer *renderer = NULL;
@@ -88,6 +89,26 @@ static int SDLCALL CompareMetadataKeys(const void *a, const void *b)
return SDL_strcmp(*(const char **) a, *(const char **) b);
}
+#if TEST_TAGS
+static void showtags(MIX_Track *track, const char *when)
+{
+ int count;
+ char **tags = MIX_GetTrackTags(track, &count);
+ if (!tags) {
+ SDL_Log("GETTRACKTAGS FAILED! %s", SDL_GetError());
+ return;
+ }
+
+ SDL_Log("Track tags %s (%d):", when, count);
+ for (int i = 0; i < count; i++) {
+ SDL_Log(" - %s", tags[i]);
+ }
+
+ SDL_assert(tags[count] == NULL);
+ SDL_free(tags);
+}
+#endif
+
SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
{
SDL_SetAppMetadata("Test SDL_mixer", "1.0", "org.libsdl.testmixer");
@@ -186,6 +207,23 @@ SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
SDL_PropertiesID options;
+ #if TEST_TAGS
+ MIX_TagTrack(track1, "Abc");
+ MIX_TagTrack(track1, "xyZ");
+ MIX_TagTrack(track1, "1234567890");
+ MIX_TagTrack(track1, "TopSecret");
+ MIX_TagTrack(track1, "MyFavoriteTrack");
+ MIX_TagTrack(track1, "Can I put spaces and punctuation in here?");
+ MIX_TagTrack(track1, "Music");
+ MIX_TagTrack(track1, "NotImportant");
+ MIX_TagTrack(track1, "Orange");
+ showtags(track1, "at startup");
+ MIX_UntagTrack(track1, "TopSecret");
+ showtags(track1, "after removing 'TopSecret'");
+ MIX_UntagTrack(track1, NULL);
+ showtags(track1, "after removing everything");
+ #endif
+
options = SDL_CreateProperties();
SDL_SetNumberProperty(options, MIX_PROP_PLAY_MAX_MILLISECONDS_NUMBER, 9440);
SDL_SetNumberProperty(options, MIX_PROP_PLAY_LOOPS_NUMBER, 3);