SDL_mixer: api: Added MIX_GetTrackTags().

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