SDL_mixer: Add support for GME (Game Music Emulator)

From 19a88666a4c973c5a97d2a50a20b01c37338588b Mon Sep 17 00:00:00 2001
From: Connor Clark <[EMAIL REDACTED]>
Date: Thu, 22 Dec 2022 14:38:37 -0800
Subject: [PATCH] Add support for GME (Game Music Emulator)

---
 configure.ac                  |  46 ++++
 include/SDL_mixer.h           |   7 +-
 src/codecs/music_cmd.c        |   2 +
 src/codecs/music_drflac.c     |   2 +
 src/codecs/music_drmp3.c      |   2 +
 src/codecs/music_flac.c       |   4 +-
 src/codecs/music_fluidsynth.c |   2 +
 src/codecs/music_gme.c        | 452 ++++++++++++++++++++++++++++++++++
 src/codecs/music_gme.h        |  26 ++
 src/codecs/music_modplug.c    |   2 +
 src/codecs/music_mpg123.c     |   2 +
 src/codecs/music_nativemidi.c |   2 +
 src/codecs/music_ogg.c        |   2 +
 src/codecs/music_ogg_stb.c    |   2 +
 src/codecs/music_opus.c       |   2 +
 src/codecs/music_timidity.c   |   2 +
 src/codecs/music_wav.c        |   6 +-
 src/codecs/music_wavpack.c    |   2 +
 src/codecs/music_xmp.c        |   2 +
 src/music.c                   |  68 +++++
 src/music.h                   |   8 +-
 21 files changed, 637 insertions(+), 6 deletions(-)
 create mode 100644 src/codecs/music_gme.c
 create mode 100644 src/codecs/music_gme.h

diff --git a/configure.ac b/configure.ac
index 64d68e39..e8bd5116 100644
--- a/configure.ac
+++ b/configure.ac
@@ -571,6 +571,52 @@ if test x$enable_music_midi_timidity != xyes -a \
     AC_MSG_WARN([MIDI support disabled])
 fi
 
+AC_ARG_ENABLE([music-gme],
+[AS_HELP_STRING([--enable-music-gme], [enable Game Music Emu support [default=no]])],
+              [], [enable_music_gme=no])
+AC_ARG_ENABLE([music-gme-shared],
+[AS_HELP_STRING([--enable-music-gme-shared], [dynamically load libgme library [default=yes]])],
+              [], [enable_music_gme_shared=yes])
+if test x$enable_music_gme = xyes; then
+    PKG_CHECK_MODULES([GME], [libgme], [dnl
+            have_gme_hdr=yes
+            have_gme_lib=yes
+            have_gme_pc=yes
+        ], [dnl
+            AC_CHECK_HEADER([gme/gme.h], [have_gme_hdr=yes])
+            AC_CHECK_LIB([gme], [gme_open_data], [have_gme_lib=yes;GME_LIBS="-lgme"])
+        ])
+    if test x$have_gme_hdr = xyes -a x$have_gme_lib = xyes; then
+        have_gme=yes
+        case "$host" in
+            *-*-darwin*)
+                gme_lib=[`find_lib libgme.dylib`]
+                ;;
+            *-*-cygwin* | *-*-mingw*)
+                gme_lib=[`find_lib "libgme*.dll"`]
+                ;;
+            *)
+                gme_lib=[`find_lib "libgme.so.*"`]
+                ;;
+        esac
+        EXTRA_CFLAGS="$EXTRA_CFLAGS -DMUSIC_GME $GME_CFLAGS"
+        if test x$enable_music_gme_shared = xyes && test x$gme_lib != x; then
+            echo "-- dynamic libgme -> $gme_lib"
+            EXTRA_CFLAGS="$EXTRA_CFLAGS -DGME_DYNAMIC=\\\"$gme_lib\\\""
+        else
+            EXTRA_LDFLAGS="$EXTRA_LDFLAGS $GME_LIBS"
+            if test x$have_gme_pc = xyes; then
+                PC_REQUIRES="$PC_REQUIRES libgme"
+            else
+                PC_LIBS="$PC_LIBS $GME_LIBS"
+            fi
+        fi
+    else
+        AC_MSG_WARN([*** Unable to find GME library (https://bitbucket.org/mpyne/game-music-emu)])
+        AC_MSG_WARN([Game Music Emu support disabled.])
+    fi
+fi
+
 AC_ARG_ENABLE([music-ogg],
 [AS_HELP_STRING([--enable-music-ogg], [enable Ogg Vorbis music [default=yes]])],
               [], [enable_music_ogg=yes])
diff --git a/include/SDL_mixer.h b/include/SDL_mixer.h
index 4231e2ae..2e8f1ab5 100644
--- a/include/SDL_mixer.h
+++ b/include/SDL_mixer.h
@@ -262,7 +262,8 @@ typedef enum {
     MUS_FLAC,
     MUS_MODPLUG_UNUSED,
     MUS_OPUS,
-    MUS_WAVPACK
+    MUS_WAVPACK,
+    MUS_GME
 } Mix_MusicType;
 
 /**
@@ -2425,6 +2426,10 @@ extern DECLSPEC int SDLCALL Mix_PausedMusic(void);
  */
 extern DECLSPEC int SDLCALL Mix_ModMusicJumpToOrder(int order);
 
+/* Tracks */
+extern DECLSPEC int SDLCALL Mix_StartTrack(Mix_Music *music, int track);
+extern DECLSPEC int SDLCALL Mix_GetNumTracks(Mix_Music *music);
+
 /**
  * Set the current position in the music stream, in seconds.
  *
diff --git a/src/codecs/music_cmd.c b/src/codecs/music_cmd.c
index a293f92a..5d129f07 100644
--- a/src/codecs/music_cmd.c
+++ b/src/codecs/music_cmd.c
@@ -293,6 +293,8 @@ Mix_MusicInterface Mix_MusicInterface_CMD =
     NULL,   /* LoopEnd */
     NULL,   /* LoopLength */
     NULL,   /* GetMetaTag */
+    NULL,   /* GetNumTracks */
+    NULL,   /* StartTrack */
     MusicCMD_Pause,
     MusicCMD_Resume,
     MusicCMD_Stop,
diff --git a/src/codecs/music_drflac.c b/src/codecs/music_drflac.c
index 635e9757..e1216ba3 100644
--- a/src/codecs/music_drflac.c
+++ b/src/codecs/music_drflac.c
@@ -409,6 +409,8 @@ Mix_MusicInterface Mix_MusicInterface_DRFLAC =
     DRFLAC_LoopEnd,
     DRFLAC_LoopLength,
     DRFLAC_GetMetaTag,
+    NULL,   /* GetNumTracks */
+    NULL,   /* StartTrack */
     NULL,   /* Pause */
     NULL,   /* Resume */
     DRFLAC_Stop,
diff --git a/src/codecs/music_drmp3.c b/src/codecs/music_drmp3.c
index 7af022d0..9e3087b2 100644
--- a/src/codecs/music_drmp3.c
+++ b/src/codecs/music_drmp3.c
@@ -282,6 +282,8 @@ Mix_MusicInterface Mix_MusicInterface_DRMP3 =
     NULL,   /* LoopEnd */
     NULL,   /* LoopLength */
     DRMP3_GetMetaTag,
+    NULL,   /* GetNumTracks */
+    NULL,   /* StartTrack */
     NULL,   /* Pause */
     NULL,   /* Resume */
     DRMP3_Stop,
diff --git a/src/codecs/music_flac.c b/src/codecs/music_flac.c
index 2e19d807..b45789fc 100644
--- a/src/codecs/music_flac.c
+++ b/src/codecs/music_flac.c
@@ -748,7 +748,9 @@ Mix_MusicInterface Mix_MusicInterface_FLAC =
     FLAC_LoopStart,
     FLAC_LoopEnd,
     FLAC_LoopLength,
-    FLAC_GetMetaTag,/* GetMetaTag */
+    FLAC_GetMetaTag,
+    NULL,   /* GetNumTracks */
+    NULL,   /* StartTrack */
     NULL,   /* Pause */
     NULL,   /* Resume */
     FLAC_Stop,   /* Stop */
diff --git a/src/codecs/music_fluidsynth.c b/src/codecs/music_fluidsynth.c
index 49d9699a..9b366fe0 100644
--- a/src/codecs/music_fluidsynth.c
+++ b/src/codecs/music_fluidsynth.c
@@ -363,6 +363,8 @@ Mix_MusicInterface Mix_MusicInterface_FLUIDSYNTH =
     NULL,   /* LoopEnd */
     NULL,   /* LoopLength */
     NULL,   /* GetMetaTag */
+    NULL,   /* GetNumTracks */
+    NULL,   /* StartTrack */
     NULL,   /* Pause */
     NULL,   /* Resume */
     FLUIDSYNTH_Stop,
diff --git a/src/codecs/music_gme.c b/src/codecs/music_gme.c
new file mode 100644
index 00000000..67569e28
--- /dev/null
+++ b/src/codecs/music_gme.c
@@ -0,0 +1,452 @@
+/*
+  SDL_mixer:  An audio mixer library based on the SDL library
+  Copyright (C) 1997-2022 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+
+#ifdef MUSIC_GME
+
+#include "SDL_loadso.h"
+
+#include "music_gme.h"
+
+#include <gme/gme.h>
+
+typedef struct {
+    int loaded;
+    void *handle;
+
+    gme_err_t (*gme_open_data)(void const* data, long size, Music_Emu** out, int sample_rate);
+    int (*gme_track_count)(Music_Emu const*);
+    gme_err_t (*gme_start_track)(Music_Emu*, int index);
+    int (*gme_track_ended)(Music_Emu const*);
+    void (*gme_set_tempo)(Music_Emu*, double tempo);
+    int (*gme_voice_count)(Music_Emu const*);
+    void (*gme_mute_voice)(Music_Emu*, int index, int mute);
+#if GME_VERSION >= 0x000700
+    void (*gme_set_fade)(Music_Emu*, int start_msec, int fade_msec);
+#else
+    void (*gme_set_fade)(Music_Emu*, int start_msec);
+#endif
+    void (*gme_set_autoload_playback_limit)(Music_Emu*, int do_autoload_limit);
+    gme_err_t (*gme_track_info)(Music_Emu const*, gme_info_t** out, int track);
+    void (*gme_free_info)(gme_info_t*);
+    gme_err_t (*gme_seek)(Music_Emu*, int msec);
+    int (*gme_tell)(Music_Emu const*);
+    gme_err_t (*gme_play)(Music_Emu*, int count, short out[]);
+    void (*gme_delete)(Music_Emu*);
+} gme_loader;
+
+static gme_loader gme;
+
+#ifdef GME_DYNAMIC
+#define FUNCTION_LOADER(FUNC, SIG) \
+    gme.FUNC = (SIG) SDL_LoadFunction(gme.handle, #FUNC); \
+    if (gme.FUNC == NULL) { SDL_UnloadObject(gme.handle); return -1; }
+#else
+#define FUNCTION_LOADER(FUNC, SIG) \
+    gme.FUNC = FUNC; \
+    if (gme.FUNC == NULL) { Mix_SetError("Missing GME.framework"); return -1; }
+#endif
+
+static int GME_Load(void)
+{
+    if (gme.loaded == 0) {
+#ifdef GME_DYNAMIC
+        gme.handle = SDL_LoadObject(GME_DYNAMIC);
+        if (gme.handle == NULL) {
+            return -1;
+        }
+#endif
+        FUNCTION_LOADER(gme_open_data, gme_err_t (*)(void const*,long,Music_Emu**,int))
+        FUNCTION_LOADER(gme_track_count, int (*)(Music_Emu const*))
+        FUNCTION_LOADER(gme_start_track, gme_err_t (*)( Music_Emu*,int))
+        FUNCTION_LOADER(gme_track_ended, int (*)( Music_Emu const*))
+        FUNCTION_LOADER(gme_set_tempo, void (*)(Music_Emu*,double))
+        FUNCTION_LOADER(gme_voice_count, int (*)(Music_Emu const*))
+        FUNCTION_LOADER(gme_mute_voice, void (*)(Music_Emu*,int,int))
+#if GME_VERSION >= 0x000700
+        FUNCTION_LOADER(gme_set_fade, void (*)(Music_Emu*,int,int))
+#else
+        FUNCTION_LOADER(gme_set_fade, void (*)(Music_Emu*,int))
+#endif
+        FUNCTION_LOADER(gme_track_info, gme_err_t (*)(Music_Emu const*, gme_info_t**, int))
+        FUNCTION_LOADER(gme_free_info, void (*)(gme_info_t*))
+        FUNCTION_LOADER(gme_seek, gme_err_t (*)(Music_Emu*,int))
+        FUNCTION_LOADER(gme_tell, int (*)(Music_Emu const*))
+        FUNCTION_LOADER(gme_play, gme_err_t (*)(Music_Emu*, int, short[]))
+        FUNCTION_LOADER(gme_delete, void (*)(Music_Emu*))
+#if defined(GME_DYNAMIC)
+        gme.gme_set_autoload_playback_limit = (void (*)(Music_Emu*,int)) SDL_LoadFunction(gme.handle, "gme_set_autoload_playback_limit");
+#elif (GME_VERSION >= 0x000603)
+        gme.gme_set_autoload_playback_limit = gme_set_autoload_playback_limit;
+#else
+        gme.gme_set_autoload_playback_limit = NULL;
+#endif
+    }
+    ++gme.loaded;
+
+    return 0;
+}
+
+static void GME_Unload(void)
+{
+    if (gme.loaded == 0) {
+        return;
+    }
+    if (gme.loaded == 1) {
+#ifdef GME_DYNAMIC
+        SDL_UnloadObject(gme.handle);
+#endif
+    }
+    --gme.loaded;
+}
+
+/* This file supports Game Music Emulator music streams */
+typedef struct
+{
+    int play_count;
+    Music_Emu* game_emu;
+    int freesrc;
+    SDL_bool has_track_length;
+    int track_length;
+    int intro_length;
+    int loop_length;
+    int volume;
+    double tempo;
+    double gain;
+    SDL_AudioStream *stream;
+    void *buffer;
+    size_t buffer_size;
+    Mix_MusicMetaTags tags;
+} GME_Music;
+
+static void GME_Delete(void *context);
+
+/* Set the volume for a GME stream */
+static void GME_SetVolume(void *music_p, int volume)
+{
+    GME_Music *music = (GME_Music*)music_p;
+    double v = SDL_floor(((double)(volume) * music->gain) + 0.5);
+    music->volume = (int)v;
+}
+
+/* Get the volume for a GME stream */
+static int GME_GetVolume(void *music_p)
+{
+    GME_Music *music = (GME_Music*)music_p;
+    double v = SDL_floor(((double)(music->volume) / music->gain) + 0.5);
+    return (int)v;
+}
+
+static int initialize_from_track_info(GME_Music *music, int track)
+{
+    gme_info_t *musInfo;
+    SDL_bool has_loop_length = SDL_TRUE;
+    const char *err;
+
+    err = gme.gme_track_info(music->game_emu, &musInfo, track);
+    if (err != 0) {
+        Mix_SetError("GME: %s", err);
+        return -1;
+    }
+
+    music->track_length = musInfo->length;
+    music->intro_length = musInfo->intro_length;
+    music->loop_length = musInfo->loop_length;
+
+    music->has_track_length = SDL_TRUE;
+    if (music->track_length <= 0 ) {
+        music->track_length = (int)(2.5 * 60 * 1000);
+        music->has_track_length = SDL_FALSE;
+    }
+
+    if (music->intro_length < 0 ) {
+        music->intro_length = 0;
+    }
+    if (music->loop_length <= 0 ) {
+        if (music->track_length > 0) {
+            music->loop_length = music->track_length;
+        } else {
+            music->loop_length = (int)(2.5 * 60 * 1000);
+        }
+        has_loop_length = SDL_FALSE;
+    }
+
+    if (!music->has_track_length && has_loop_length) {
+        music->track_length = music->intro_length + music->loop_length;
+        music->has_track_length = SDL_TRUE;
+    }
+
+    meta_tags_set(&music->tags, MIX_META_TITLE, musInfo->song);
+    meta_tags_set(&music->tags, MIX_META_ARTIST, musInfo->author);
+    meta_tags_set(&music->tags, MIX_META_ALBUM, musInfo->game);
+    meta_tags_set(&music->tags, MIX_META_COPYRIGHT, musInfo->copyright);
+    gme.gme_free_info(musInfo);
+
+    return 0;
+}
+
+static void *GME_CreateFromRW(struct SDL_RWops *src, int freesrc)
+{
+    void *mem = 0;
+    size_t size;
+    GME_Music *music;
+    const char *err;
+
+    if (src == NULL) {
+        Mix_SetError("GME: Empty source given");
+        return NULL;
+    }
+
+    music = (GME_Music *)SDL_calloc(1, sizeof(GME_Music));
+
+    music->tempo = 1.0;
+    music->gain = 1.0;
+
+    music->stream = SDL_NewAudioStream(AUDIO_S16SYS, 2, music_spec.freq,
+                                       music_spec.format, music_spec.channels, music_spec.freq);
+    if (!music->stream) {
+        GME_Delete(music);
+        return NULL;
+    }
+
+    music->buffer_size = music_spec.samples * sizeof(Sint16) * 2/*channels*/ * music_spec.channels;
+    music->buffer = SDL_malloc(music->buffer_size);
+    if (!music->buffer) {
+        SDL_OutOfMemory();
+        GME_Delete(music);
+        return NULL;
+    }
+
+    SDL_RWseek(src, 0, RW_SEEK_SET);
+    mem = SDL_LoadFile_RW(src, &size, SDL_FALSE);
+    if (mem) {
+        err = gme.gme_open_data(mem, size, &music->game_emu, music_spec.freq);
+        SDL_free(mem);
+        if (err != 0) {
+            GME_Delete(music);
+            Mix_SetError("GME: %s", err);
+            return NULL;
+        }
+    } else {
+        SDL_OutOfMemory();
+        GME_Delete(music);
+        return NULL;
+    }
+
+    /* Set this flag BEFORE calling the gme_start_track() to fix an inability to loop forever */
+    if (gme.gme_set_autoload_playback_limit) {
+        gme.gme_set_autoload_playback_limit(music->game_emu, 0);
+    }
+
+    err = gme.gme_start_track(music->game_emu, 0);
+    if (err != 0) {
+        GME_Delete(music);
+        Mix_SetError("GME: %s", err);
+        return NULL;
+    }
+
+    gme.gme_set_tempo(music->game_emu, music->tempo);
+
+    music->volume = MIX_MAX_VOLUME;
+
+    meta_tags_init(&music->tags);
+    if (initialize_from_track_info(music, 0) == -1) {
+        GME_Delete(music);
+        return NULL;
+    }
+
+    music->freesrc = freesrc;
+    return music;
+}
+
+/* Start playback of a given Game Music Emulators stream */
+static int GME_Play(void *music_p, int play_count)
+{
+    GME_Music *music = (GME_Music*)music_p;
+    int fade_start;
+    if (music) {
+        SDL_AudioStreamClear(music->stream);
+        music->play_count = play_count;
+        fade_start = play_count > 0 ? music->intro_length + (music->loop_length * play_count) : -1;
+#if GME_VERSION >= 0x000700
+        gme.gme_set_fade(music->game_emu, fade_start, 8000);
+#else
+        gme.gme_set_fade(music->game_emu, fade_start);
+#endif
+        gme.gme_seek(music->game_emu, 0);
+    }
+    return 0;
+}
+
+static int GME_GetSome(void *context, void *data, int bytes, SDL_bool *done)
+{
+    GME_Music *music = (GME_Music*)context;
+    int filled;
+    const char *err = NULL;
+
+    filled = SDL_AudioStreamGet(music->stream, data, bytes);
+    if (filled != 0) {
+        return filled;
+    }
+
+    if (gme.gme_track_ended(music->game_emu)) {
+        /* All done */
+        *done = SDL_TRUE;
+        return 0;
+    }
+
+    err = gme.gme_play(music->game_emu, (music->buffer_size / 2), (short*)music->buffer);
+    if (err != NULL) {
+        Mix_SetError("GME: %s", err);
+        return 0;
+    }
+
+    if (SDL_AudioStreamPut(music->stream, music->buffer, music->buffer_size) < 0) {
+        return -1;
+    }
+    return 0;
+}
+
+/* Play some of a stream previously started with GME_Play() */
+static int GME_PlayAudio(void *music_p, void *data, int bytes)
+{
+    GME_Music *music = (GME_Music*)music_p;
+    return music_pcm_getaudio(music_p, data, bytes, music->volume, GME_GetSome);
+}
+
+/* Close the given Game Music Emulators stream */
+static void GME_Delete(void *context)
+{
+    GME_Music *music = (GME_Music*)context;
+    if (music) {
+        meta_tags_clear(&music->tags);
+        if (music->game_emu && music->freesrc) {
+            gme.gme_delete(music->game_emu);
+            music->game_emu = NULL;
+        }
+        if (music->stream) {
+            SDL_FreeAudioStream(music->stream);
+        }
+        if (music->buffer) {
+            SDL_free(music->buffer);
+        }
+        SDL_free(music);
+    }
+}
+
+// TODO: this should accept a track number, not assume the current track!
+static const char* GME_GetMetaTag(void *context, Mix_MusicMetaTag tag_type)
+{
+    GME_Music *music = (GME_Music *)context;
+    return meta_tags_get(&music->tags, tag_type);
+}
+
+/* Jump (seek) to a given position (time is in seconds) */
+static int GME_Seek(void *music_p, double time)
+{
+    GME_Music *music = (GME_Music*)music_p;
+    gme.gme_seek(music->game_emu, (int)(SDL_floor((time * 1000.0) + 0.5)));
+    return 0;
+}
+
+static double GME_Tell(void *music_p)
+{
+    GME_Music *music = (GME_Music*)music_p;
+    return (double)(gme.gme_tell(music->game_emu)) / 1000.0;
+}
+
+static double GME_Duration(void *music_p)
+{
+    GME_Music *music = (GME_Music*)music_p;
+    if (music->has_track_length) {
+        return (double)(music->track_length) / 1000.0;
+    } else {
+
+        return -1.0;
+    }
+}
+
+static int GME_GetNumTracks(void *music_p)
+{
+    GME_Music *music = (GME_Music *)music_p;
+    return gme.gme_track_count(music->game_emu);
+}
+
+static int GME_StartTrack(void *music_p, int track)
+{
+    GME_Music *music = (GME_Music *)music_p;
+    const char *err;
+
+    if ((track < 0) || (track >= gme.gme_track_count(music->game_emu))) {
+        track = gme.gme_track_count(music->game_emu) - 1;
+    }
+
+    err = gme.gme_start_track(music->game_emu, track);
+    if (err != 0) {
+        Mix_SetError("GME: %s", err);
+        return -1;
+    }
+
+    GME_Play(music, music->play_count);
+
+    if (initialize_from_track_info(music, track) == -1) {
+        return -1;
+    }
+
+    return 0;
+}
+
+
+Mix_MusicInterface Mix_MusicInterface_GME =
+{
+    "GME",
+    MIX_MUSIC_GME,
+    MUS_GME,
+    SDL_FALSE,
+    SDL_FALSE,
+
+    GME_Load,
+    NULL,   /* Open */
+    GME_CreateFromRW,
+    NULL,   /* CreateFromFile */
+    GME_SetVolume,
+    GME_GetVolume,
+    GME_Play,
+    NULL,   /* IsPlaying */
+    GME_PlayAudio,
+    NULL,   /* Jump */
+    GME_Seek,
+    GME_Tell,
+    GME_Duration,
+    NULL,
+    NULL,
+    NULL,
+    GME_GetMetaTag,
+    GME_GetNumTracks,
+    GME_StartTrack,
+    NULL,   /* Pause */
+    NULL,   /* Resume */
+    NULL,   /* Stop */
+    GME_Delete,
+    NULL,   /* Close */
+    GME_Unload
+};
+
+#endif /* MUSIC_GME */
diff --git a/src/codecs/music_gme.h b/src/codecs/music_gme.h
new file mode 100644
index 00000000..e0c85b7b
--- /dev/null
+++ b/src/codecs/music_gme.h
@@ -0,0 +1,26 @@
+/*
+  SDL_mixer:  An audio mixer library based on the SDL library
+  Copyright (C) 1997-2020 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+
+/* This file supports playing chiptune files with libGME */
+
+#include "music.h"
+
+extern Mix_MusicInterface Mix_MusicInterface_GME;
diff --git a/src/codecs/music_modplug.c b/src/codecs/music_modplug.c
index b8d2936e..d6985d5f 100644
--- a/src/codecs/music_modplug.c
+++ b/src/codecs/music_modplug.c
@@ -366,6 +366,8 @@ Mix_MusicInterface Mix_MusicInterface_MODPLUG =
     NULL,   /* LoopEnd */
     NULL,   /* LoopLength */
     MODPLUG_GetMetaTag,
+    NULL,   /* GetNumTracks */
+    NULL,   /* StartTrack */
     NULL,   /* Pause */
     NULL,   /* Resume */
     MODPLUG_Stop,
diff --git a/src/codecs/music_mpg123.c b/src/codecs/music_mpg123.c
index a8198d8a..49cd090e 100644
--- a/src/codecs/music_mpg123.c
+++ b/src/codecs/music_mpg123.c
@@ -534,6 +534,8 @@ Mix_MusicInterface Mix_MusicInterface_MPG123 =
     NULL,   /* LoopEnd */
     NULL,   /* LoopLength */
     MPG123_GetMetaTag,
+    NULL,   /* GetNumTracks */
+    NULL,   /* StartTrack */
     NULL,   /* Pause */
     NULL,   /* Resume */
     MPG123_Stop,
diff --git a/src/codecs/music_nativemidi.c b/src/codecs/music_nativemidi.c
index 22fbcc05..308fda15 100644
--- a/src/codecs/music_nativemidi.c
+++ b/src/codecs/music_nativemidi.c
@@ -108,6 +108,8 @@ Mix_MusicInterface Mix_MusicInterface_NATIVEMIDI =
     NULL,   /* LoopEnd */
     NULL,   /* LoopLength */
     NULL,   /* GetMetaTag */
+    NULL,   /* GetNumTracks */
+    NULL,   /* StartTrack */
     NATIVEMIDI_Pause,
     NATIVEMIDI_Resume,
     NATIVEMIDI_Stop,
diff --git a/src/codecs/music_ogg.c b/src/codecs/music_ogg.c
index 16af47b9..09f60a83 100644
--- a/src/codecs/music_ogg.c
+++ b/src/codecs/music_ogg.c
@@ -543,6 +543,8 @@ Mix_MusicInterface Mix_MusicInterface_OGG =
     OGG_LoopEnd,
     OGG_LoopLength,
     OGG_GetMetaTag,   /* GetMetaTag */
+    NULL,   /* GetNumTracks */
+    NULL,   /* StartTrack */
     NULL,   /* Pause */
     NULL,   /* Resume */
     OGG_Stop,
diff --git a/src/codecs/music_ogg_stb.c b/src/codecs/music_ogg_stb.c
index b33f38e0..84db5b54 100644
--- a/src/codecs/music_ogg_stb.c
+++ b/src/codecs/music_ogg_stb.c
@@ -472,6 +472,8 @@ Mix_MusicInterface Mix_MusicInterface_OGG =
     OGG_LoopEnd,
     OGG_LoopLength,
     OGG_GetMetaTag,   /* GetMetaTag */
+    NULL,   /* GetNumTracks */
+    NULL,   /* StartTrack */
     NULL,   /* Pause */
     NULL,   /* Resume */
     OGG_Stop,
diff --git a/src/codecs/music_opus.c b/src/codecs/music_opus.c
index 1afdc9d0..b5488eb7 100644
--- a/src/codecs/music_opus.c
+++ b/src/codecs/music_opus.c
@@ -515,6 +515,8 @@ Mix_MusicInterface Mix_MusicInterface_Opus =
     OPUS_LoopEnd,
     OPUS_LoopLength,
     OPUS_GetMetaTag,
+    NULL,   /* GetNumTracks */
+    NULL,   /* StartTrack */
     NULL,   /* Pause */
     NULL,   /* Resume */
     OPUS_Stop,
diff --git a/src/codecs/music_timidity.c b/src/codecs/music_timidity.c
index 5822c4bf..6e6baa04 100644
--- a/src/codecs/music_timidity.c
+++ b/src/codecs/music_timidity.c
@@ -284,6 +284,8 @@ Mix_MusicInterface Mix_MusicInterface_TIMIDITY =
     NULL,   /* LoopEnd */
     NULL,   /* LoopLength */
     NULL,   /* GetMetaTag */
+    NULL,   /* GetNumTracks */
+    NULL,   /* StartTrack */
     NULL,   /* Pause */
     NULL,   /* Resume */
     TIMIDITY_Stop,
diff --git a/src/codecs/music_wav.c b/src/codecs/music_wav.c
index e4140c2f..1d826cf8 100644
--- a/src/codecs/music_wav.c
+++ b/src/codecs/music_wav.c
@@ -931,8 +931,7 @@ static SDL_bool LoadWAVMusic(WAV_Music *wave)
         if (chunk_length == 0)
             break;
 
-        switch (chunk_type)
-        {
+        switch (chunk_type) {
         case FMT:
             found_FMT = SDL_TRUE;
             if (!ParseFMT(wave, chunk_length))
@@ -1135,7 +1134,6 @@ static SDL_bool LoadAIFFMusic(WAV_Music *wave)
         return SDL_FALSE;
     }
 
-
     wave->samplesize = channels * (samplesize / 8);
     wave->stop = wave->start + channels * numsamples * (samplesize / 8);
 
@@ -1254,6 +1252,8 @@ Mix_MusicInterface Mix_MusicInterface_WAV =
     NULL,   /* LoopEnd */
     NULL,   /* LoopLength */
     WAV_GetMetaTag,   /* GetMetaTag */
+    NULL,   /* GetNumTracks */
+    NULL,   /* StartTrack */
     NULL,   /* Pause */
     NULL,   /* Resume */
     WAV_Stop, /* Stop */
diff --git a/src/codecs/music_wavpack.c b/src/codecs/music_wavpack.c
index 4444498f..4e318a96 100644
--- a/src/codecs/music_wavpack.c
+++ b/src/codecs/music_wavpack.c
@@ -726,6 +726,8 @@ Mix_MusicInterface Mix_MusicInterface_WAVPACK =
     NULL, /* LoopEnd */
     NULL, /* LoopLength */
     WAVPACK_GetMetaTag,
+    NULL,   /* GetNumTracks */
+    NULL,   /* StartTrack */
     NULL,   /* Pause */
     NULL,   /* Resume */
     WAVPACK_Stop,
diff --git a/src/codecs/music_xmp.c b/src/codecs/music_xmp.c
index cce2e6dd..9e3d2430 100644
--- a/src/codecs/music_xmp.c
+++ b/src/codecs/music_xmp.c
@@ -429,6 +429,8 @@ Mix_MusicInterface Mix_MusicInterface_XMP =
     NULL,   /* LoopEnd */
     NULL,   /* LoopLength */
     XMP_GetMetaTag,
+    NULL,   /* GetNumTracks */
+    NULL,   /* StartTrack */
     NULL,   /* Pause */
     NULL,   /* Resume */
     XMP_Stop,
diff --git a/src/music.c b/src/music.c
index c1cba2bc..416bdbeb 100644
--- a/src/music.c
+++ b/src/music.c
@@ -40,6 +40,7 @@
 #include "music_drflac.h"
 #include "music_flac.h"
 #include "music_wavpack.h"
+#include "music_gme.h"
 #include "native_midi/native_midi.h"
 
 #include "utils.h"
@@ -201,6 +202,9 @@ static Mix_MusicInterface *s_music_interfaces[] =
 #ifdef MUSIC_MID_NATIVE
     &Mix_MusicInterface_NATIVEMIDI,
 #endif
+#ifdef MUSIC_GME
+    &Mix_MusicInterface_GME,
+#endif
 };
 
 int get_num_music_interfaces(void)
@@ -593,6 +597,32 @@ Mix_MusicType detect_music_type(SDL_RWops *src)
         return MUS_MP3;
     }
 
+    /* GME Specific files */
+    if (SDL_memcmp(magic, "ZXAY", 4) == 0)
+        return MUS_GME;
+    if (SDL_memcmp(magic, "GBS\x01", 4) == 0)
+        return MUS_GME;
+    if (SDL_memcmp(magic, "GYMX", 4) == 0)
+        return MUS_GME;
+    if (SDL_memcmp(magic, "HESM", 4) == 0)
+        return MUS_GME;
+    if (SDL_memcmp(magic, "KSCC", 4) == 0)
+        return MUS_GME;
+    if (SDL_memcmp(magic, "KSSX", 4) == 0)
+        return MUS_GME;
+    if (SDL_memcmp(magic, "NESM", 4) == 0)
+        return MUS_GME;
+    if (SDL_memcmp(magic, "NSFE", 4) == 0)
+        return MUS_GME;
+    if (SDL_memcmp(magic, "SAP\x0D", 4) == 0)
+        return MUS_GME;
+    if (SDL_memcmp(magic, "SNES", 4) == 0)
+        return MUS_GME;
+    if (SDL_memcmp(magic, "Vgm ", 4) == 0)
+        return MUS_GME;
+    if (SDL_memcmp(magic, "\x1f\x8b", 2) == 0)
+        return MUS_GME;
+
     /* Assume MOD format.
      *
      * Apparently there is no way to check if the file is really a MOD,
@@ -686,6 +716,12 @@ Mix_Music *Mix_LoadMUS(const char *file)
                     SDL_strcasecmp(ext, "WOW") == 0 ||
                     SDL_strcasecmp(ext, "XM") == 0) {
             type = MUS_MOD;
+        } else if (SDL_strcasecmp(ext, "GBS") == 0 ||
+                    SDL_strcasecmp(ext, "M3U") == 0 ||
+                    SDL_strcasecmp(ext, "NSF") == 0 ||
+                    SDL_strcasecmp(ext, "SPC") == 0 ||
+                    SDL_strcasecmp(ext, "VGM") == 0) {
+            type = MUS_GME;
         }
     }
     return Mix_LoadMUSType_RW(src, type, SDL_TRUE);
@@ -1291,6 +1327,38 @@ int Mix_PausedMusic(void)
     return (music_active == SDL_FALSE);
 }
 
+int Mix_StartTrack(Mix_Music *music, int track)
+{
+    int result;
+
+    Mix_LockAudio();
+    if (music && music->interface->StartTrack) {
+        if (music->interface->Pause) {
+            music->interface->Pause(music->context);
+        }
+        result = music->interface->StartTrack(music->context, track);
+    } else {
+        result = Mix_SetError("That operation is not supported");
+    }
+    Mix_UnlockAudio();
+
+    return result;
+}
+
+int Mix_GetNumTracks(Mix_Music *music)
+{
+    int result;
+
+    Mix_LockAudio();
+    if (music && music->interface->GetNumTracks) {
+        result = music->interface->GetNumTracks(music->context);
+    } else {
+        result = Mix_SetError("That operation is not supported");
+    }
+    Mix_UnlockAudio();
+    return result;
+}
+
 /* Check the status of the music */
 static SDL_bool music_internal_playing(void)
 {
diff --git a/src/music.h b/src/music.h
index b193e0ec..8c3afccc 100644
--- a/src/music.h
+++ b/src/music.h
@@ -41,6 +41,7 @@ typedef enum
     MIX_MUSIC_OPUS,
     MIX_MUSIC_LIBXMP,
     MIX_MUSIC_WAVPACK,
+    MIX_MUSIC_GME,
     MIX_MUSIC_LAST
 } Mix_MusicAPI;
 
@@ -133,6 +134,12 @@ typedef struct
     /* Get a meta-tag string if available */
     const char* (*GetMetaTag)(void *music, Mix_MusicMetaTag tag_type);
 
+    /* Get number of tracks. Returns -1 if not applicable */
+    int (*GetNumTracks)(void *music);
+
+    /* Start a specific track */
+    int (*StartTrack)(void *music, int track);
+
     /* Pause playing music */
     void (*Pause)(void *music);
 
@@ -150,7 +157,6 @@ typedef struct
 
     /* Unload the library */
     void (*Unload)(void);
-
 } Mix_MusicInterface;