diff --git a/.circleci/config.yml b/.circleci/config.yml
index 3129c3bdfd6b135e71152b46c7e5e373ca06a1cf..c3674a9e5f0b9b85916cbf930d98ee735de5cdd2 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -36,7 +36,7 @@ jobs:
             - v1-SRB2-APT
       - run:
           name: Install SDK
-          command: apt-get -qq -y --no-install-recommends install git build-essential nasm libpng12-dev:i386 libsdl2-mixer-dev:i386 libgme-dev:i386 gettext ccache wget gcc-multilib upx
+          command: apt-get -qq -y --no-install-recommends install git build-essential nasm libpng12-dev:i386 libsdl2-mixer-dev:i386 libgme-dev:i386 gettext ccache wget gcc-multilib upx openssh-client
       - save_cache:
           key: v1-SRB2-APT
           paths:
diff --git a/.travis.yml b/.travis.yml
index 1131bff3a7725aad321d3a12a0f3455f8561f1d0..4bfc58860abede871e6481666f97186ce865cc59 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -15,6 +15,7 @@ matrix:
               - p7zip-full
               - gcc-4.4
           compiler: gcc-4.4
+          env: GCC44=1
           #gcc-4.4 (Ubuntu/Linaro 4.4.7-8ubuntu1) 4.4.7
         - os: linux
           addons:
@@ -27,6 +28,7 @@ matrix:
               - p7zip-full
               - gcc-4.6
           compiler: gcc-4.6
+          env: GCC46=1
           #gcc-4.6 (Ubuntu/Linaro 4.6.4-6ubuntu2) 4.6.4
         - os: linux
           addons:
@@ -39,9 +41,11 @@ matrix:
               - p7zip-full
               - gcc-4.7
           compiler: gcc-4.7
+          env: GCC47=1
           #gcc-4.7
         - os: linux
           compiler: gcc
+          env: GCC48=1
           #gcc (Ubuntu 4.8.4-2ubuntu1~14.04) 4.8.4
         - os: linux
           addons:
@@ -56,6 +60,7 @@ matrix:
               - p7zip-full
               - gcc-4.8
           compiler: gcc-4.8
+          env: GCC48=1
           #gcc-4.8 (Ubuntu 4.8.5-2ubuntu1~14.04.1) 4.8.5
         - os: linux
           addons:
@@ -70,7 +75,7 @@ matrix:
               - p7zip-full
               - gcc-7
           compiler: gcc-7
-          env: WFLAGS="-Wno-tautological-compare -Wno-error=implicit-fallthrough -Wno-implicit-fallthrough"
+          env: WFLAGS="-Wno-tautological-compare -Wno-error=implicit-fallthrough -Wno-implicit-fallthrough" GCC72=1
           #gcc-7 (Ubuntu 7.2.0-1ubuntu1~14.04) 7.2.0 20170802
         - os: linux
           addons:
@@ -85,7 +90,7 @@ matrix:
               - p7zip-full
               - gcc-8
           compiler: gcc-8
-          env: WFLAGS="-Wno-tautological-compare -Wno-error=implicit-fallthrough -Wno-implicit-fallthrough -Wno-error=format-overflow"
+          env: WFLAGS="-Wno-tautological-compare -Wno-error=implicit-fallthrough -Wno-implicit-fallthrough -Wno-error=format-overflow" GCC81=1
           #gcc-8 (Ubuntu 7.2.0-1ubuntu1~14.04) 8.1.0
         - os: linux
           compiler: clang
diff --git a/src/Makefile.cfg b/src/Makefile.cfg
index d127a81cc8eeb0644fef5c8df5ca4046814a8731..2b05b70970881b83886f0b6001e40579f9283cc4 100644
--- a/src/Makefile.cfg
+++ b/src/Makefile.cfg
@@ -7,6 +7,10 @@
 # and other things
 #
 
+ifdef GCC81
+GCC80=1
+endif
+
 ifdef GCC80
 GCC72=1
 endif
@@ -112,6 +116,7 @@ WFLAGS+=-Wfloat-equal
 #WFLAGS+=-Wtraditional
 ifdef VCHELP
  WFLAGS+=-Wdeclaration-after-statement
+ WFLAGS+=-Wno-error=declaration-after-statement
 endif
  WFLAGS+=-Wundef
 ifndef GCC295
@@ -181,12 +186,6 @@ ifdef GCC46
 WFLAGS+=-Wno-suggest-attribute=noreturn
 endif
 
-ifndef MINGW
-ifdef GCC45
-WFLAGS+=-Wunsuffixed-float-constants
-endif
-endif
-
 ifdef NOLDWARNING
 LDFLAGS+=-Wl,--as-needed
 endif
@@ -200,6 +199,9 @@ WFLAGS+=$(OLDWFLAGS)
 ifdef GCC43
  #WFLAGS+=-Wno-error=clobbered
 endif
+ifdef GCC44
+ WFLAGS+=-Wno-error=array-bounds
+endif
 ifdef GCC46
  WFLAGS+=-Wno-error=suggest-attribute=noreturn
 endif
diff --git a/src/android/i_sound.c b/src/android/i_sound.c
index 2bb304424af8b8dafe3dfc9df46eef877ef4cc89..b5a1c364618fbde5b8142eb74f7541ff2ecd18be 100644
--- a/src/android/i_sound.c
+++ b/src/android/i_sound.c
@@ -96,6 +96,37 @@ boolean I_SetSongSpeed(float speed)
         return false;
 }
 
+/// ------------------------
+//  MUSIC SEEKING
+/// ------------------------
+
+UINT32 I_GetSongLength(void)
+{
+        return 0;
+}
+
+boolean I_SetSongLoopPoint(UINT32 looppoint)
+{
+        (void)looppoint;
+        return false;
+}
+
+UINT32 I_GetSongLoopPoint(void)
+{
+	return 0;
+}
+
+boolean I_SetSongPosition(UINT32 position)
+{
+        (void)position;
+        return false;
+}
+
+UINT32 I_GetSongPosition(void)
+{
+        return 0;
+}
+
 /// ------------------------
 //  MUSIC PLAYBACK
 /// ------------------------
@@ -140,3 +171,44 @@ void I_SetMusicVolume(INT32 volume)
 {
         (void)volume;
 }
+
+/// ------------------------
+//  MUSIC FADING
+/// ------------------------
+
+void I_SetInternalMusicVolume(UINT8 volume)
+{
+	(void)volume;
+}
+
+void I_StopFadingSong(void)
+{
+}
+
+boolean I_FadeSongFromVolume(UINT8 target_volume, UINT8 source_volume, UINT32 ms, void (*callback)(void));
+{
+	(void)target_volume;
+	(void)source_volume;
+	(void)ms;
+        return false;
+}
+
+boolean I_FadeSong(UINT8 target_volume, UINT32 ms, void (*callback)(void));
+{
+	(void)target_volume;
+	(void)ms;
+	return false;
+}
+
+boolean I_FadeOutStopSong(UINT32 ms)
+{
+        (void)ms;
+        return false;
+}
+
+boolean I_FadeInPlaySong(UINT32 ms, boolean looping)
+{
+        (void)ms;
+        (void)looping;
+        return false;
+}
diff --git a/src/dehacked.c b/src/dehacked.c
index 7b162c38a0e4122f950045476c77529903b7475b..54dfc441efe50095994f726ce89ea0679d9ab841 100644
--- a/src/dehacked.c
+++ b/src/dehacked.c
@@ -1160,6 +1160,13 @@ static void readlevelheader(MYFILE *f, INT32 num)
 #endif
 			else if (fastcmp(word, "MUSICTRACK"))
 				mapheaderinfo[num-1]->mustrack = ((UINT16)i - 1);
+			else if (fastcmp(word, "MUSICPOS"))
+				mapheaderinfo[num-1]->muspos = (UINT32)get_number(word2);
+			else if (fastcmp(word, "MUSICINTERFADEOUT"))
+				mapheaderinfo[num-1]->musinterfadeout = (UINT32)get_number(word2);
+			else if (fastcmp(word, "MUSICINTER"))
+				deh_strlcpy(mapheaderinfo[num-1]->musintername, word2,
+					sizeof(mapheaderinfo[num-1]->musintername), va("Level header %d: intermission music", num));
 			else if (fastcmp(word, "FORCECHARACTER"))
 			{
 				strlcpy(mapheaderinfo[num-1]->forcecharacter, word2, SKINNAMESIZE+1);
@@ -1264,6 +1271,13 @@ static void readlevelheader(MYFILE *f, INT32 num)
 				else
 					mapheaderinfo[num-1]->levelflags &= ~LF_SAVEGAME;
 			}
+			else if (fastcmp(word, "MIXNIGHTSCOUNTDOWN"))
+			{
+				if (i || word2[0] == 'T' || word2[0] == 'Y')
+					mapheaderinfo[num-1]->levelflags |= LF_MIXNIGHTSCOUNTDOWN;
+				else
+					mapheaderinfo[num-1]->levelflags &= ~LF_MIXNIGHTSCOUNTDOWN;
+			}
 
 			// Individual triggers for menu flags
 			else if (fastcmp(word, "HIDDEN"))
@@ -1460,6 +1474,10 @@ static void readcutscenescene(MYFILE *f, INT32 num, INT32 scenenum)
 			{
 				cutscenes[num]->scene[scenenum].musswitchflags = ((UINT16)i) & MUSIC_TRACKMASK;
 			}
+			else if (fastcmp(word, "MUSICPOS"))
+			{
+				cutscenes[num]->scene[scenenum].musswitchposition = (UINT32)get_number(word2);
+			}
 			else if (fastcmp(word, "MUSICLOOP"))
 			{
 				cutscenes[num]->scene[scenenum].musicloop = (UINT8)(i || word2[0] == 'T' || word2[0] == 'Y');
@@ -7793,6 +7811,7 @@ struct {
 
 	// doomdef.h constants
 	{"TICRATE",TICRATE},
+	{"MUSICRATE",MUSICRATE},
 	{"RING_DIST",RING_DIST},
 	{"PUSHACCEL",PUSHACCEL},
 	{"MODID",MODID}, // I don't know, I just thought it would be cool for a wad to potentially know what mod it was loaded into.
@@ -7879,6 +7898,7 @@ struct {
 	{"LF_NORELOAD",LF_NORELOAD},
 	{"LF_NOZONE",LF_NOZONE},
 	{"LF_SAVEGAME",LF_SAVEGAME},
+	{"LF_MIXNIGHTSCOUNTDOWN",LF_MIXNIGHTSCOUNTDOWN},
 	// And map flags
 	{"LF2_HIDEINMENU",LF2_HIDEINMENU},
 	{"LF2_HIDEINSTATS",LF2_HIDEINSTATS},
@@ -9249,6 +9269,9 @@ static inline int lib_getenum(lua_State *L)
 	} else if (fastcmp(word,"mapmusflags")) {
 		lua_pushinteger(L, mapmusflags);
 		return 1;
+	} else if (fastcmp(word,"mapmusposition")) {
+		lua_pushinteger(L, mapmusposition);
+		return 1;
 	} else if (fastcmp(word,"server")) {
 		if ((!multiplayer || !netgame) && !playeringame[serverplayer])
 			return 0;
diff --git a/src/djgppdos/i_sound.c b/src/djgppdos/i_sound.c
index 52c90aac2d2450ede35eb9773e8b9a222e661783..88b5986221496295ae86e81015db75df94bd2a07 100644
--- a/src/djgppdos/i_sound.c
+++ b/src/djgppdos/i_sound.c
@@ -438,6 +438,37 @@ boolean I_SetSongSpeed(float speed)
 	return false;
 }
 
+/// ------------------------
+// MUSIC SEEKING
+/// ------------------------
+
+UINT32 I_GetSongLength(void)
+{
+	return 0;
+}
+
+boolean I_SetSongLoopPoint(UINT32 looppoint)
+{
+        (void)looppoint;
+        return false;
+}
+
+UINT32 I_GetSongLoopPoint(void)
+{
+	return 0;
+}
+
+boolean I_SetSongPosition(UINT32 position)
+{
+    (void)position;
+    return false;
+}
+
+UINT32 I_GetSongPosition(void)
+{
+    return 0;
+}
+
 /// ------------------------
 //  MUSIC PLAYBACK
 /// ------------------------
@@ -545,3 +576,44 @@ int I_QrySongPlaying(int handle)
 	return (midi_pos==-1);
 }
 #endif
+
+/// ------------------------
+// MUSIC FADING
+/// ------------------------
+
+void I_SetInternalMusicVolume(UINT8 volume)
+{
+	(void)volume;
+}
+
+void I_StopFadingSong(void)
+{
+}
+
+boolean I_FadeSongFromVolume(UINT8 target_volume, UINT8 source_volume, UINT32 ms, void (*callback)(void));
+{
+	(void)target_volume;
+	(void)source_volume;
+	(void)ms;
+	return false;
+}
+
+boolean I_FadeSong(UINT8 target_volume, UINT32 ms, void (*callback)(void));
+{
+	(void)target_volume;
+	(void)ms;
+	return false;
+}
+
+boolean I_FadeOutStopSong(UINT32 ms)
+{
+	(void)ms;
+	return false;
+}
+
+boolean I_FadeInPlaySong(UINT32 ms, boolean looping)
+{
+        (void)ms;
+        (void)looping;
+        return false;
+}
diff --git a/src/doomdef.h b/src/doomdef.h
index 9ef1a43f0ef4ec63961e7c99621a3d43b45218bc..512c90f86781245ee7191feb19f1c688c849abe5 100644
--- a/src/doomdef.h
+++ b/src/doomdef.h
@@ -373,6 +373,8 @@ typedef enum
 #define NEWTICRATERATIO 1 // try 4 for 140 fps :)
 #define NEWTICRATE (TICRATE*NEWTICRATERATIO)
 
+#define MUSICRATE 1000 // sound timing is calculated by milliseconds
+
 #define RING_DIST 512*FRACUNIT // how close you need to be to a ring to attract it
 
 #define PUSHACCEL (2*FRACUNIT) // Acceleration for MF2_SLIDEPUSH items.
diff --git a/src/doomstat.h b/src/doomstat.h
index 713a739091b7ebebe1276a5194dabba79dc60ccb..aa8ce58f1e408665287df5eb4601a4ed66e42959 100644
--- a/src/doomstat.h
+++ b/src/doomstat.h
@@ -33,8 +33,10 @@
 extern INT16 gamemap;
 extern char mapmusname[7];
 extern UINT16 mapmusflags;
+extern UINT32 mapmusposition;
 #define MUSIC_TRACKMASK   0x0FFF // ----************
 #define MUSIC_RELOADRESET 0x8000 // *---------------
+#define MUSIC_FORCERESET  0x4000 // -*--------------
 // Use other bits if necessary.
 
 extern INT16 maptol;
@@ -160,6 +162,7 @@ typedef struct
 
 	char   musswitch[7];
 	UINT16 musswitchflags;
+	UINT32 musswitchposition;
 
 	UINT8 fadecolor; // Color number for fade, 0 means don't do the first fade
 	UINT8 fadeinid;  // ID of the first fade, to a color -- ignored if fadecolor is 0
@@ -284,6 +287,7 @@ typedef struct
 	INT16 nextlevel;       ///< Map number of next level, or 1100-1102 to end.
 	char musname[7];       ///< Music track to play. "" for no music.
 	UINT16 mustrack;       ///< Subsong to play. Only really relevant for music modules and specific formats supported by GME. 0 to ignore.
+	UINT32 muspos;    ///< Music position to jump to.
 	char forcecharacter[17];  ///< (SKINNAMESIZE+1) Skin to switch to or "" to disable.
 	UINT8 weather;         ///< 0 = sunny day, 1 = storm, 2 = snow, 3 = rain, 4 = blank, 5 = thunder w/o rain, 6 = rain w/o lightning, 7 = heat wave.
 	INT16 skynum;          ///< Sky number to use.
@@ -305,7 +309,7 @@ typedef struct
 	SINT8 bonustype;      ///< What type of bonus does this level have? (-1 for null.)
 	SINT8 maxbonuslives;  ///< How many bonus lives to award at Intermission? (-1 for unlimited.)
 
-	UINT8 levelflags;     ///< LF_flags:  merged eight booleans into one UINT8 for space, see below
+	UINT8 levelflags;     ///< LF_flags:  merged booleans into one UINT8 for space, see below
 	UINT8 menuflags;      ///< LF2_flags: options that affect record attack / nights mode menus
 
 	char selectheading[22]; ///< Level select heading. Allows for controllable grouping.
@@ -318,6 +322,10 @@ typedef struct
 	UINT8 numGradedMares;   ///< Internal. For grade support.
 	nightsgrades_t *grades; ///< NiGHTS grades. Allocated dynamically for space reasons. Be careful.
 
+	// Music stuff.
+	UINT32 musinterfadeout;  ///< Fade out level music on intermission screen in milliseconds
+	char musintername[7];    ///< Intermission screen music.
+
 	// Lua stuff.
 	// (This is not ifdeffed so the map header structure can stay identical, just in case.)
 	UINT8 numCustomOptions;     ///< Internal. For Lua custom value support.
@@ -331,6 +339,7 @@ typedef struct
 #define LF_NORELOAD       8 ///< Don't reload level on death
 #define LF_NOZONE        16 ///< Don't include "ZONE" on level title
 #define LF_SAVEGAME      32 ///< Save the game upon loading this level
+#define LF_MIXNIGHTSCOUNTDOWN 64 ///< Play sfx_timeup instead of music change for NiGHTS countdown
 
 #define LF2_HIDEINMENU     1 ///< Hide in the multiplayer menu
 #define LF2_HIDEINSTATS    2 ///< Hide in the statistics screen
diff --git a/src/dummy/i_sound.c b/src/dummy/i_sound.c
index 7275bb1ae5d9345e035aa775b602b5fb3bf4f8a1..f09158e013639c8a506058a0eaa7b1259d1285c6 100644
--- a/src/dummy/i_sound.c
+++ b/src/dummy/i_sound.c
@@ -95,6 +95,37 @@ boolean I_SetSongSpeed(float speed)
 	return false;
 }
 
+/// ------------------------
+//  MUSIC SEEKING
+/// ------------------------
+
+UINT32 I_GetSongLength(void)
+{
+	return 0;
+}
+
+boolean I_SetSongLoopPoint(UINT32 looppoint)
+{
+        (void)looppoint;
+        return false;
+}
+
+UINT32 I_GetSongLoopPoint(void)
+{
+	return 0;
+}
+
+boolean I_SetSongPosition(UINT32 position)
+{
+    (void)position;
+    return false;
+}
+
+UINT32 I_GetSongPosition(void)
+{
+    return 0;
+}
+
 /// ------------------------
 //  MUSIC PLAYBACK
 /// ------------------------
@@ -142,4 +173,45 @@ boolean I_SetSongTrack(int track)
 {
 	(void)track;
 	return false;
-}
\ No newline at end of file
+}
+
+/// ------------------------
+//  MUSIC FADING
+/// ------------------------
+
+void I_SetInternalMusicVolume(UINT8 volume)
+{
+	(void)volume;
+}
+
+void I_StopFadingSong(void)
+{
+}
+
+boolean I_FadeSongFromVolume(UINT8 target_volume, UINT8 source_volume, UINT32 ms, void (*callback)(void));
+{
+	(void)target_volume;
+	(void)source_volume;
+	(void)ms;
+	return false;
+}
+
+boolean I_FadeSong(UINT8 target_volume, UINT32 ms, void (*callback)(void));
+{
+	(void)target_volume;
+	(void)ms;
+	return false;
+}
+
+boolean I_FadeOutStopSong(UINT32 ms)
+{
+	(void)ms;
+	return false;
+}
+
+boolean I_FadeInPlaySong(UINT32 ms, boolean looping)
+{
+        (void)ms;
+        (void)looping;
+        return false;
+}
diff --git a/src/f_finale.c b/src/f_finale.c
index e45d9f53105de4bfc9730cdd1562ce25d23e2a85..07f5ab439760148c669db87515ac9d5a63afb0f8 100644
--- a/src/f_finale.c
+++ b/src/f_finale.c
@@ -1875,9 +1875,10 @@ static void F_AdvanceToNextScene(void)
 	picypos = cutscenes[cutnum]->scene[scenenum].ycoord[picnum];
 
 	if (cutscenes[cutnum]->scene[scenenum].musswitch[0])
-		S_ChangeMusic(cutscenes[cutnum]->scene[scenenum].musswitch,
+		S_ChangeMusicEx(cutscenes[cutnum]->scene[scenenum].musswitch,
 			cutscenes[cutnum]->scene[scenenum].musswitchflags,
-			cutscenes[cutnum]->scene[scenenum].musicloop);
+			cutscenes[cutnum]->scene[scenenum].musicloop,
+			cutscenes[cutnum]->scene[scenenum].musswitchposition, 0, 0);
 
 	// Fade to the next
 	dofadenow = true;
@@ -1951,9 +1952,10 @@ void F_StartCustomCutscene(INT32 cutscenenum, boolean precutscene, boolean reset
 	stoptimer = 0;
 
 	if (cutscenes[cutnum]->scene[0].musswitch[0])
-		S_ChangeMusic(cutscenes[cutnum]->scene[0].musswitch,
+		S_ChangeMusicEx(cutscenes[cutnum]->scene[0].musswitch,
 			cutscenes[cutnum]->scene[0].musswitchflags,
-			cutscenes[cutnum]->scene[0].musicloop);
+			cutscenes[cutnum]->scene[0].musicloop,
+			cutscenes[cutnum]->scene[scenenum].musswitchposition, 0, 0);
 	else
 		S_StopMusic();
 }
diff --git a/src/g_game.c b/src/g_game.c
index 80a75c6d0708b1c3ff7382996b61f17dcca4ff97..4a9dedf508ca82b65c50edee9e0896a15c3ad577 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -71,6 +71,7 @@ static void G_DoWorldDone(void);
 
 char   mapmusname[7]; // Music name
 UINT16 mapmusflags; // Track and reset bit
+UINT32 mapmusposition; // Position to jump to
 
 INT16 gamemap = 1;
 INT16 maptol;
@@ -2292,9 +2293,14 @@ void G_PlayerReborn(INT32 player)
 		{
 			strncpy(mapmusname, mapheaderinfo[gamemap-1]->musname, 7);
 			mapmusname[6] = 0;
-			mapmusflags = mapheaderinfo[gamemap-1]->mustrack & MUSIC_TRACKMASK;
+			mapmusflags = (mapheaderinfo[gamemap-1]->mustrack & MUSIC_TRACKMASK);
+			mapmusposition = mapheaderinfo[gamemap-1]->muspos;
 		}
-		S_ChangeMusic(mapmusname, mapmusflags, true);
+
+		// This is in S_Start, but this was not here previously.
+		// if (cv_resetmusic.value)
+		// 	S_StopMusic();
+		S_ChangeMusicEx(mapmusname, mapmusflags, true, mapmusposition, 0, 0);
 	}
 
 	if (gametype == GT_COOP)
diff --git a/src/i_sound.h b/src/i_sound.h
index fd73d14541b4e61a1cfc8717051d83e7a3d855fb..9a5c2930afeb6a6af98fb22b8d5b0296c2446bc7 100644
--- a/src/i_sound.h
+++ b/src/i_sound.h
@@ -146,6 +146,18 @@ boolean I_SongPaused(void);
 
 boolean I_SetSongSpeed(float speed);
 
+/// ------------------------
+//  MUSIC SEEKING
+/// ------------------------
+
+UINT32 I_GetSongLength(void);
+
+boolean I_SetSongLoopPoint(UINT32 looppoint);
+UINT32 I_GetSongLoopPoint(void);
+
+boolean I_SetSongPosition(UINT32 position);
+UINT32 I_GetSongPosition(void);
+
 /// ------------------------
 //  MUSIC PLAYBACK
 /// ------------------------
@@ -216,6 +228,17 @@ void I_SetMusicVolume(UINT8 volume);
 
 boolean I_SetSongTrack(INT32 track);
 
+/// ------------------------
+/// MUSIC FADING
+/// ------------------------
+
+void I_SetInternalMusicVolume(UINT8 volume);
+void I_StopFadingSong(void);
+boolean I_FadeSongFromVolume(UINT8 target_volume, UINT8 source_volume, UINT32 ms, void (*callback)(void));
+boolean I_FadeSong(UINT8 target_volume, UINT32 ms, void (*callback)(void));
+boolean I_FadeOutStopSong(UINT32 ms);
+boolean I_FadeInPlaySong(UINT32 ms, boolean looping);
+
 /// ------------------------
 //  CD MUSIC I/O
 /// ------------------------
diff --git a/src/lua_baselib.c b/src/lua_baselib.c
index a8598582cc0a6326a47580aff84077c0bbf593b8..755b76835390f6b6df7c3fb111f81a3445d7a08e 100644
--- a/src/lua_baselib.c
+++ b/src/lua_baselib.c
@@ -1015,7 +1015,8 @@ static int lib_pRestoreMusic(lua_State *L)
 	INLEVEL
 	if (!player)
 		return LUA_ErrInvalid(L, "player_t");
-	P_RestoreMusic(player);
+	if (P_IsLocalPlayer(player))
+		P_RestoreMusic(player);
 	return 0;
 }
 
@@ -2283,7 +2284,7 @@ static int lib_sChangeMusic(lua_State *L)
 {
 #ifdef MUSICSLOT_COMPATIBILITY
 	const char *music_name;
-	UINT32 music_num;
+	UINT32 music_num, position, prefadems, fadeinms;
 	char music_compat_name[7];
 
 	boolean looping;
@@ -2311,7 +2312,6 @@ static int lib_sChangeMusic(lua_State *L)
 		music_name = luaL_checkstring(L, 1);
 	}
 
-
 	looping = (boolean)lua_opttrueboolean(L, 2);
 
 #else
@@ -2336,8 +2336,12 @@ static int lib_sChangeMusic(lua_State *L)
 #endif
 	music_flags = (UINT16)luaL_optinteger(L, 4, 0);
 
+	position = (UINT32)luaL_optinteger(L, 5, 0);
+	prefadems = (UINT32)luaL_optinteger(L, 6, 0);
+	fadeinms = (UINT32)luaL_optinteger(L, 7, 0);
+
 	if (!player || P_IsLocalPlayer(player))
-		S_ChangeMusic(music_name, music_flags, looping);
+		S_ChangeMusicEx(music_name, music_flags, looping, position, prefadems, fadeinms);
 	return 0;
 }
 
@@ -2354,10 +2358,8 @@ static int lib_sSpeedMusic(lua_State *L)
 			return LUA_ErrInvalid(L, "player_t");
 	}
 	if (!player || P_IsLocalPlayer(player))
-		lua_pushboolean(L, S_SpeedMusic(speed));
-	else
-		lua_pushboolean(L, false);
-	return 1;
+		S_SpeedMusic(speed);
+	return 0;
 }
 
 static int lib_sStopMusic(lua_State *L)
@@ -2375,6 +2377,110 @@ static int lib_sStopMusic(lua_State *L)
 	return 0;
 }
 
+static int lib_sSetInternalMusicVolume(lua_State *L)
+{
+	UINT32 volume = (UINT32)luaL_checkinteger(L, 1);
+	player_t *player = NULL;
+	NOHUD
+	if (!lua_isnone(L, 2) && lua_isuserdata(L, 2))
+	{
+		player = *((player_t **)luaL_checkudata(L, 2, META_PLAYER));
+		if (!player)
+			return LUA_ErrInvalid(L, "player_t");
+	}
+	if (!player || P_IsLocalPlayer(player))
+	{
+		S_SetInternalMusicVolume(volume);
+		lua_pushboolean(L, true);
+	}
+	else
+		lua_pushnil(L);
+	return 1;
+}
+
+static int lib_sStopFadingMusic(lua_State *L)
+{
+	player_t *player = NULL;
+	NOHUD
+	if (!lua_isnone(L, 1) && lua_isuserdata(L, 1))
+	{
+		player = *((player_t **)luaL_checkudata(L, 1, META_PLAYER));
+		if (!player)
+			return LUA_ErrInvalid(L, "player_t");
+	}
+	if (!player || P_IsLocalPlayer(player))
+	{
+		S_StopFadingMusic();
+		lua_pushboolean(L, true);
+	}
+	else
+		lua_pushnil(L);
+	return 1;
+}
+
+static int lib_sFadeMusic(lua_State *L)
+{
+	UINT32 target_volume = (UINT32)luaL_checkinteger(L, 1);
+	UINT32 ms;
+	INT32 source_volume;
+	player_t *player = NULL;
+	NOHUD
+	if (!lua_isnone(L, 3) && lua_isuserdata(L, 3))
+	{
+		player = *((player_t **)luaL_checkudata(L, 3, META_PLAYER));
+		if (!player)
+			return LUA_ErrInvalid(L, "player_t");
+		ms = (UINT32)luaL_checkinteger(L, 2);
+		source_volume = -1;
+	}
+	else if (!lua_isnone(L, 4) && lua_isuserdata(L, 4))
+	{
+		player = *((player_t **)luaL_checkudata(L, 4, META_PLAYER));
+		if (!player)
+			return LUA_ErrInvalid(L, "player_t");
+		source_volume = (INT32)luaL_checkinteger(L, 2);
+		ms = (UINT32)luaL_checkinteger(L, 3);
+	}
+	else if (luaL_optinteger(L, 3, INT32_MAX) == INT32_MAX)
+	{
+		ms = (UINT32)luaL_checkinteger(L, 2);
+		source_volume = -1;
+	}
+	else
+	{
+		source_volume = (INT32)luaL_checkinteger(L, 2);
+		ms = (UINT32)luaL_checkinteger(L, 3);
+	}
+
+	NOHUD
+
+	if (!player || P_IsLocalPlayer(player))
+		lua_pushboolean(L, S_FadeMusicFromVolume(target_volume, source_volume, ms));
+	else
+		lua_pushnil(L);
+	return 1;
+}
+
+static int lib_sFadeOutStopMusic(lua_State *L)
+{
+	UINT32 ms = (UINT32)luaL_checkinteger(L, 1);
+	player_t *player = NULL;
+	NOHUD
+	if (!lua_isnone(L, 2) && lua_isuserdata(L, 2))
+	{
+		player = *((player_t **)luaL_checkudata(L, 2, META_PLAYER));
+		if (!player)
+			return LUA_ErrInvalid(L, "player_t");
+	}
+	if (!player || P_IsLocalPlayer(player))
+	{
+		lua_pushboolean(L, S_FadeOutStopMusic(ms));
+	}
+	else
+		lua_pushnil(L);
+	return 1;
+}
+
 static int lib_sOriginPlaying(lua_State *L)
 {
 	void *origin = *((mobj_t **)luaL_checkudata(L, 1, META_MOBJ));
@@ -2782,6 +2888,10 @@ static luaL_Reg lib[] = {
 	{"S_ChangeMusic",lib_sChangeMusic},
 	{"S_SpeedMusic",lib_sSpeedMusic},
 	{"S_StopMusic",lib_sStopMusic},
+	{"S_SetInternalMusicVolume", lib_sSetInternalMusicVolume},
+	{"S_StopFadingMusic",lib_sStopFadingMusic},
+	{"S_FadeMusic",lib_sFadeMusic},
+	{"S_FadeOutStopMusic",lib_sFadeOutStopMusic},
 	{"S_OriginPlaying",lib_sOriginPlaying},
 	{"S_IdPlaying",lib_sIdPlaying},
 	{"S_SoundPlaying",lib_sSoundPlaying},
diff --git a/src/lua_maplib.c b/src/lua_maplib.c
index f04490d9dc457f8bb59efb9568ddd704a19f89ea..27b97204f2f64a073005ae22dc1a41aea86b572f 100644
--- a/src/lua_maplib.c
+++ b/src/lua_maplib.c
@@ -2031,6 +2031,12 @@ static int mapheaderinfo_get(lua_State *L)
 		lua_pushstring(L, header->musname);
 	else if (fastcmp(field,"mustrack"))
 		lua_pushinteger(L, header->mustrack);
+	else if (fastcmp(field,"muspos"))
+		lua_pushinteger(L, header->muspos);
+	else if (fastcmp(field,"musinterfadeout"))
+		lua_pushinteger(L, header->musinterfadeout);
+	else if (fastcmp(field,"musintername"))
+		lua_pushstring(L, header->musintername);
 	else if (fastcmp(field,"forcecharacter"))
 		lua_pushstring(L, header->forcecharacter);
 	else if (fastcmp(field,"weather"))
diff --git a/src/p_inter.c b/src/p_inter.c
index c46ef407d00655409322ea39c58aa67c5d268e1e..2471ce8c727a8021c6aa5120884f2b3f7ad0af3a 100644
--- a/src/p_inter.c
+++ b/src/p_inter.c
@@ -2756,8 +2756,18 @@ static inline void P_NiGHTSDamage(mobj_t *target, mobj_t *source)
 		if (oldnightstime > 10*TICRATE
 			&& player->nightstime < 10*TICRATE)
 		{
-			//S_StartSound(NULL, sfx_timeup); // that creepy "out of time" music from NiGHTS. Dummied out, as some on the dev team thought it wasn't Sonic-y enough (Mystic, notably). Uncomment to restore. -SH
-			S_ChangeMusicInternal((((maptol & TOL_NIGHTS) && !G_IsSpecialStage(gamemap)) ? "_ntime" : "_drown"), false);
+			if ((mapheaderinfo[gamemap-1]->levelflags & LF_MIXNIGHTSCOUNTDOWN)
+#ifdef _WIN32
+				// win32 MIDI volume hack means we cannot fade down the music
+				&& S_MusicType() != MU_MID
+#endif
+			)
+			{
+				S_FadeMusic(0, 10*MUSICRATE);
+				S_StartSound(NULL, sfx_timeup); // that creepy "out of time" music from NiGHTS.
+			}
+			else
+				S_ChangeMusicInternal((((maptol & TOL_NIGHTS) && !G_IsSpecialStage(gamemap)) ? "_ntime" : "_drown"), false);
 		}
 	}
 }
@@ -3137,8 +3147,13 @@ void P_SpecialStageDamage(player_t *player, mobj_t *inflictor, mobj_t *source)
 	if (oldnightstime > 10*TICRATE
 		&& player->nightstime < 10*TICRATE)
 	{
-		//S_StartSound(NULL, sfx_timeup); // that creepy "out of time" music from NiGHTS. Dummied out, as some on the dev team thought it wasn't Sonic-y enough (Mystic, notably). Uncomment to restore. -SH
-		S_ChangeMusicInternal((((maptol & TOL_NIGHTS) && !G_IsSpecialStage(gamemap)) ? "_ntime" : "_drown"), false);
+		if (mapheaderinfo[gamemap-1]->levelflags & LF_MIXNIGHTSCOUNTDOWN)
+		{
+			S_FadeMusic(0, 10*MUSICRATE);
+			S_StartSound(NULL, sfx_timeup); // that creepy "out of time" music from NiGHTS.
+		}
+		else
+			S_ChangeMusicInternal((((maptol & TOL_NIGHTS) && !G_IsSpecialStage(gamemap)) ? "_ntime" : "_drown"), false);
 	}
 }
 
diff --git a/src/p_setup.c b/src/p_setup.c
index b17199eb16d7b78c3e0b63eabc396e9aa44a550b..3dd6739069e62997c6e9d66a19be8043b0fe2742 100644
--- a/src/p_setup.c
+++ b/src/p_setup.c
@@ -212,6 +212,9 @@ static void P_ClearSingleMapHeaderInfo(INT16 i)
 	snprintf(mapheaderinfo[num]->musname, 7, "%sM", G_BuildMapName(i));
 	mapheaderinfo[num]->musname[6] = 0;
 	mapheaderinfo[num]->mustrack = 0;
+	mapheaderinfo[num]->muspos = 0;
+	mapheaderinfo[num]->musinterfadeout = 0;
+	mapheaderinfo[num]->musintername[0] = '\0';
 	mapheaderinfo[num]->forcecharacter[0] = '\0';
 	mapheaderinfo[num]->weather = 0;
 	mapheaderinfo[num]->skynum = 1;
@@ -1428,19 +1431,33 @@ static void P_LoadRawSideDefs2(void *data)
 				{
 					M_Memcpy(process,msd->bottomtexture,8);
 					process[8] = '\0';
-					sd->bottomtexture = get_number(process)-1;
+					sd->bottomtexture = get_number(process);
 				}
-				M_Memcpy(process,msd->toptexture,8);
-				process[8] = '\0';
-				sd->text = Z_Malloc(7, PU_LEVEL, NULL);
-
-				// If they type in O_ or D_ and their music name, just shrug,
-				// then copy the rest instead.
-				if ((process[0] == 'O' || process[0] == 'D') && process[7])
-					M_Memcpy(sd->text, process+2, 6);
-				else // Assume it's a proper music name.
-					M_Memcpy(sd->text, process, 6);
-				sd->text[6] = 0;
+
+				if (!(msd->midtexture[0] == '-' && msd->midtexture[1] == '\0') || msd->midtexture[1] != '\0')
+				{
+					M_Memcpy(process,msd->midtexture,8);
+					process[8] = '\0';
+					sd->midtexture = get_number(process);
+				}
+
+				// always process if back sidedef, because we need that - symbol
+ 				sd->text = Z_Malloc(7, PU_LEVEL, NULL);
+				if (i == 1 || msd->toptexture[0] != '-' || msd->toptexture[1] != '\0')
+				{
+					M_Memcpy(process,msd->toptexture,8);
+					process[8] = '\0';
+
+					// If they type in O_ or D_ and their music name, just shrug,
+					// then copy the rest instead.
+					if ((process[0] == 'O' || process[0] == 'D') && process[7])
+						M_Memcpy(sd->text, process+2, 6);
+					else // Assume it's a proper music name.
+						M_Memcpy(sd->text, process, 6);
+					sd->text[6] = 0;
+				}
+				else
+					sd->text[0] = 0;
 				break;
 			}
 
diff --git a/src/p_spec.c b/src/p_spec.c
index fb678a4208d1868c8fa346ec03a70696929d4334..58eb8a8d9d1a70d78a6dbe35cd99185283d4f111 100644
--- a/src/p_spec.c
+++ b/src/p_spec.c
@@ -2593,16 +2593,68 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 			// console player only unless NOCLIMB is set
 			if ((line->flags & ML_NOCLIMB) || (mo && mo->player && P_IsLocalPlayer(mo->player)))
 			{
-				UINT16 tracknum = (UINT16)sides[line->sidenum[0]].bottomtexture;
+				boolean musicsame = (!sides[line->sidenum[0]].text[0] || !strnicmp(sides[line->sidenum[0]].text, S_MusicName(), 7));
+				UINT16 tracknum = (UINT16)max(sides[line->sidenum[0]].bottomtexture, 0);
+				INT32 position = (INT32)max(sides[line->sidenum[0]].midtexture, 0);
+				UINT32 prefadems = (UINT32)max(sides[line->sidenum[0]].textureoffset >> FRACBITS, 0);
+				UINT32 postfadems = (UINT32)max(sides[line->sidenum[0]].rowoffset >> FRACBITS, 0);
+				UINT8 fadetarget = (UINT8)max((line->sidenum[1] != 0xffff) ? sides[line->sidenum[1]].textureoffset >> FRACBITS : 0, 0);
+				INT16 fadesource = (INT16)max((line->sidenum[1] != 0xffff) ? sides[line->sidenum[1]].rowoffset >> FRACBITS : -1, -1);
+
+				// Seek offset from current song position
+				if (line->flags & ML_EFFECT1)
+				{
+					// adjust for loop point if subtracting
+					if (position < 0 && S_GetMusicLength() &&
+						S_GetMusicPosition() > S_GetMusicLoopPoint() &&
+						S_GetMusicPosition() + position < S_GetMusicLoopPoint())
+						position = max(S_GetMusicLength() - (S_GetMusicLoopPoint() - (S_GetMusicPosition() + position)), 0);
+					else
+						position = max(S_GetMusicPosition() + position, 0);
+				}
+
+				// Fade current music to target volume (if music won't be changed)
+				if ((line->flags & ML_EFFECT2) && fadetarget && musicsame)
+				{
+					// 0 fadesource means fade from current volume.
+					// meaning that we can't specify volume 0 as the source volume -- this starts at 1.
+					if (!fadesource)
+						fadesource = -1;
 
-				strncpy(mapmusname, sides[line->sidenum[0]].text, 7);
-				mapmusname[6] = 0;
+					if (!postfadems)
+						S_SetInternalMusicVolume(fadetarget);
+					else
+						S_FadeMusicFromVolume(fadetarget, fadesource, postfadems);
 
-				mapmusflags = tracknum & MUSIC_TRACKMASK;
-				if (!(line->flags & ML_BLOCKMONSTERS))
-					mapmusflags |= MUSIC_RELOADRESET;
+					if (position)
+						S_SetMusicPosition(position);
+				}
+				// Change the music and apply position/fade operations
+				else
+				{
+					strncpy(mapmusname, sides[line->sidenum[0]].text, 7);
+					mapmusname[6] = 0;
 
-				S_ChangeMusic(mapmusname, mapmusflags, !(line->flags & ML_EFFECT4));
+					mapmusflags = tracknum & MUSIC_TRACKMASK;
+					if (!(line->flags & ML_BLOCKMONSTERS))
+						mapmusflags |= MUSIC_RELOADRESET;
+					if (line->flags & ML_BOUNCY)
+						mapmusflags |= MUSIC_FORCERESET;
+
+					mapmusposition = position;
+
+					S_ChangeMusicEx(mapmusname, mapmusflags, !(line->flags & ML_EFFECT4), position,
+						!(line->flags & ML_EFFECT2) ? prefadems : 0,
+						!(line->flags & ML_EFFECT2) ? postfadems : 0);
+
+					if ((line->flags & ML_EFFECT2) && fadetarget)
+					{
+						if (!postfadems)
+							S_SetInternalMusicVolume(fadetarget);
+						else
+							S_FadeMusicFromVolume(fadetarget, fadesource, postfadems);
+					}
+				}
 
 				// Except, you can use the ML_BLOCKMONSTERS flag to change this behavior.
 				// if (mapmusflags & MUSIC_RELOADRESET) then it will reset the music in G_PlayerReborn.
diff --git a/src/p_user.c b/src/p_user.c
index 30e9a19fdfa8f8388753050248f945e11be5ca99..ca14c64d4367a04e53b2afe60787885d21e6cd7b 100644
--- a/src/p_user.c
+++ b/src/p_user.c
@@ -629,6 +629,14 @@ static void P_DeNightserizePlayer(player_t *player)
 			player->marescore = player->spheres =\
 			 player->rings = 0;
 			P_DamageMobj(player->mo, NULL, NULL, 1, DMG_INSTAKILL);
+
+			// Reset music to beginning if MIXNIGHTSCOUNTDOWN
+			if ((mapheaderinfo[gamemap-1]->levelflags & LF_MIXNIGHTSCOUNTDOWN)
+#ifdef _WIN32
+				&& S_MusicType() != MU_MID
+#endif
+			)
+				S_SetMusicPosition(0);
 		}
 
 		break;
@@ -639,7 +647,24 @@ static void P_DeNightserizePlayer(player_t *player)
 	player->oldscale = 0;
 
 	// Restore from drowning music
-	P_RestoreMusic(player);
+	if ((mapheaderinfo[gamemap-1]->levelflags & LF_MIXNIGHTSCOUNTDOWN)
+#ifdef _WIN32
+		&& S_MusicType() != MU_MID
+#endif
+	)
+	{
+		S_StopSoundByNum(sfx_timeup);
+		S_StopFadingMusic();
+		S_SetInternalMusicVolume(100);
+
+		// Reset the music if you did not destroy all the capsules, because you failed.
+		// Why make the all-capsules exception: because it's your reward for nearly finishing the level!
+		// (unless the player auto-loses upon denightserizing; for death case, see above.)
+		if (P_FindLowestMare() != UINT8_MAX || G_IsSpecialStage(gamemap))
+			S_SetMusicPosition(0);
+	}
+	else
+		P_RestoreMusic(player);
 
 	P_RunDeNightserizeExecutors(player->mo);
 }
@@ -687,7 +712,16 @@ void P_NightserizePlayer(player_t *player, INT32 nighttime)
 	player->nightstime = player->startedtime = player->lapstartedtime = nighttime*TICRATE;
 	player->bonustime = false;
 
-	P_RestoreMusic(player);
+	// Restore from drowning music
+	if (mapheaderinfo[gamemap-1]->levelflags & LF_MIXNIGHTSCOUNTDOWN)
+	{
+		S_StopSoundByNum(sfx_timeup);
+		S_StopFadingMusic();
+		S_SetInternalMusicVolume(100);
+	}
+	else
+		P_RestoreMusic(player);
+
 	P_SetPlayerMobjState(player->mo, S_PLAY_NIGHTS_TRANS1);
 
 	if (gametype == GT_RACE || gametype == GT_COMPETITION)
@@ -1292,13 +1326,13 @@ void P_RestoreMusic(player_t *player)
 		if (mapheaderinfo[gamemap-1]->levelflags & LF_SPEEDMUSIC)
 		{
 			S_SpeedMusic(1.4f);
-			S_ChangeMusic(mapmusname, mapmusflags, true);
+			S_ChangeMusicEx(mapmusname, mapmusflags, true, mapmusposition, 0, 0);
 		}
 		else
 			S_ChangeMusicInternal("_shoes", true);
 	}
 	else
-		S_ChangeMusic(mapmusname, mapmusflags, true);
+		S_ChangeMusicEx(mapmusname, mapmusflags, true, mapmusposition, 0, 0);
 }
 
 //
@@ -6344,14 +6378,18 @@ static void P_NiGHTSMovement(player_t *player)
 	{
 		P_DeNightserizePlayer(player);
 		S_StartScreamSound(player->mo, sfx_s3k66);
-//		S_StopSoundByNum(sfx_timeup); // Kill the "out of time" music, if it's playing. Dummied out, as some on the dev team thought it wasn't Sonic-y enough (Mystic, notably). Uncomment to restore. -SH
-		P_RestoreMusic(player); // I have my doubts that this is the right place for this...
-
 		return;
 	}
 	else if (P_IsLocalPlayer(player) && player->nightstime == 10*TICRATE)
-//		S_StartSound(NULL, sfx_timeup); // that creepy "out of time" music from NiGHTS. Dummied out, as some on the dev team thought it wasn't Sonic-y enough (Mystic, notably). Uncomment to restore. -SH
-		S_ChangeMusicInternal((((maptol & TOL_NIGHTS) && !G_IsSpecialStage(gamemap)) ? "_ntime" : "_drown"), false);
+	{
+		if (mapheaderinfo[gamemap-1]->levelflags & LF_MIXNIGHTSCOUNTDOWN)
+		{
+			S_FadeMusic(0, 10*MUSICRATE);
+			S_StartSound(NULL, sfx_timeup); // that creepy "out of time" music from NiGHTS.
+		}
+		else
+			S_ChangeMusicInternal((((maptol & TOL_NIGHTS) && !G_IsSpecialStage(gamemap)) ? "_ntime" : "_drown"), false);
+	}
 
 	if (player->mo->z < player->mo->floorz)
 		player->mo->z = player->mo->floorz;
diff --git a/src/r_segs.c b/src/r_segs.c
index f0c8bc09355a519d44a81167729c3f84dc4886af..246d7ad00e678fa2af4c46b6d6225b9782b5a22e 100644
--- a/src/r_segs.c
+++ b/src/r_segs.c
@@ -2818,12 +2818,23 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 			ffloor[i].f_pos >>= 4;
 #ifdef ESLOPE
 			ffloor[i].f_pos_slope >>= 4;
-			ffloor[i].f_frac = (centeryfrac>>4) - FixedMul(ffloor[i].f_pos, rw_scale);
-			ffloor[i].f_step = ((centeryfrac>>4) - FixedMul(ffloor[i].f_pos_slope, ds_p->scale2) - ffloor[i].f_frac)/(range);
+#endif
+			if (linedef->special == 41) // Horizon lines extend FOFs in contact with them too.
+			{
+				ffloor[i].f_step = 0;
+				ffloor[i].f_frac = (centeryfrac>>4);
+				topfrac++; // Prevent 1px HOM
+			}
+			else
+			{
+#ifdef ESLOPE
+				ffloor[i].f_frac = (centeryfrac>>4) - FixedMul(ffloor[i].f_pos, rw_scale);
+				ffloor[i].f_step = ((centeryfrac>>4) - FixedMul(ffloor[i].f_pos_slope, ds_p->scale2) - ffloor[i].f_frac)/(range);
 #else
-			ffloor[i].f_step = FixedMul(-rw_scalestep, ffloor[i].f_pos);
-			ffloor[i].f_frac = (centeryfrac>>4) - FixedMul(ffloor[i].f_pos, rw_scale);
+				ffloor[i].f_step = FixedMul(-rw_scalestep, ffloor[i].f_pos);
+				ffloor[i].f_frac = (centeryfrac>>4) - FixedMul(ffloor[i].f_pos, rw_scale);
 #endif
+			}
 		}
 	}
 
diff --git a/src/s_sound.c b/src/s_sound.c
index ebed3068a9d15537408dff942449063e0e5afc8c..93588f081854547961103526599cdbda2c132a75 100644
--- a/src/s_sound.c
+++ b/src/s_sound.c
@@ -39,6 +39,10 @@ extern INT32 msg_id;
 #include "fastcmp.h"
 #include "m_misc.h" // for tunes command
 
+#if defined(HAVE_BLUA) && defined(HAVE_LUA_MUSICPLUS)
+#include "lua_hook.h" // MusicChange hook
+#endif
+
 #ifdef HW3SOUND
 // 3D Sound Interface
 #include "hardware/hw3sound.h"
@@ -1373,6 +1377,12 @@ static void      *music_data;
 static UINT16    music_flags;
 static boolean   music_looping;
 
+static char      queue_name[7];
+static UINT16    queue_flags;
+static boolean   queue_looping;
+static UINT32    queue_position;
+static UINT32    queue_fadeinms;
+
 /// ------------------------
 /// Music Status
 /// ------------------------
@@ -1407,6 +1417,11 @@ musictype_t S_MusicType(void)
 	return I_SongType();
 }
 
+const char *S_MusicName(void)
+{
+	return music_name;
+}
+
 boolean S_MusicInfo(char *mname, UINT16 *mflags, boolean *looping)
 {
 	if (!I_SongPlaying())
@@ -1437,6 +1452,35 @@ boolean S_SpeedMusic(float speed)
 	return I_SetSongSpeed(speed);
 }
 
+/// ------------------------
+/// Music Seeking
+/// ------------------------
+
+UINT32 S_GetMusicLength(void)
+{
+	return I_GetSongLength();
+}
+
+boolean S_SetMusicLoopPoint(UINT32 looppoint)
+{
+	return I_SetSongLoopPoint(looppoint);
+}
+
+UINT32 S_GetMusicLoopPoint(void)
+{
+	return I_GetSongLoopPoint();
+}
+
+boolean S_SetMusicPosition(UINT32 position)
+{
+	return I_SetSongPosition(position);
+}
+
+UINT32 S_GetMusicPosition(void)
+{
+	return I_GetSongPosition();
+}
+
 /// ------------------------
 /// Music Playback
 /// ------------------------
@@ -1509,12 +1553,13 @@ static void S_UnloadMusic(void)
 	music_looping = false;
 }
 
-static boolean S_PlayMusic(boolean looping)
+static boolean S_PlayMusic(boolean looping, UINT32 fadeinms)
 {
 	if (S_MusicDisabled())
 		return false;
 
-	if (!I_PlaySong(looping))
+	if ((!fadeinms && !I_PlaySong(looping)) ||
+		(fadeinms && !I_FadeInPlaySong(fadeinms, looping)))
 	{
 		S_UnloadMusic();
 		return false;
@@ -1524,38 +1569,93 @@ static boolean S_PlayMusic(boolean looping)
 	return true;
 }
 
-void S_ChangeMusic(const char *mmusic, UINT16 mflags, boolean looping)
+static void S_QueueMusic(const char *mmusic, UINT16 mflags, boolean looping, UINT32 position, UINT32 fadeinms)
 {
+	strncpy(queue_name, mmusic, 7);
+	queue_flags = mflags;
+	queue_looping = looping;
+	queue_position = position;
+	queue_fadeinms = fadeinms;
+}
+
+static void S_ClearQueue(void)
+{
+	queue_name[0] = queue_flags = queue_looping = queue_position = queue_fadeinms = 0;
+}
+
+static void S_ChangeMusicToQueue(void)
+{
+	S_ChangeMusicEx(queue_name, queue_flags, queue_looping, queue_position, 0, queue_fadeinms);
+	S_ClearQueue();
+}
+
+void S_ChangeMusicEx(const char *mmusic, UINT16 mflags, boolean looping, UINT32 position, UINT32 prefadems, UINT32 fadeinms)
+{
+	char newmusic[7];
+
 	if (S_MusicDisabled())
 		return;
 
-	// No Music (empty string)
-	if (mmusic[0] == 0)
-	{
-		S_StopMusic();
+	strncpy(newmusic, mmusic, 7);
+#if defined(HAVE_BLUA) && defined(HAVE_LUA_MUSICPLUS)
+	if(LUAh_MusicChange(music_name, newmusic, &mflags, &looping, &position, &prefadems, &fadeinms))
+		return;
+#endif
+	newmusic[6] = 0;
+
+ 	// No Music (empty string)
+	if (newmusic[0] == 0)
+ 	{
+		if (prefadems)
+			I_FadeSong(0, prefadems, &S_StopMusic);
+		else
+			S_StopMusic();
 		return;
 	}
 
-	if (strnicmp(music_name, mmusic, 6))
+	if (prefadems && S_MusicPlaying()) // queue music change for after fade // allow even if the music is the same
 	{
-		S_StopMusic(); // shutdown old music
+		CONS_Debug(DBG_DETAILED, "Now fading out song %s\n", music_name);
+		S_QueueMusic(newmusic, mflags, looping, position, fadeinms);
+		I_FadeSong(0, prefadems, S_ChangeMusicToQueue);
+		return;
+	}
+	else if (strnicmp(music_name, newmusic, 6) || (mflags & MUSIC_FORCERESET))
+ 	{
+		CONS_Debug(DBG_DETAILED, "Now playing song %s\n", newmusic);
 
-		if (!S_LoadMusic(mmusic))
+		S_StopMusic();
+
+		if (!S_LoadMusic(newmusic))
 		{
-			CONS_Alert(CONS_ERROR, "Music %.6s could not be loaded!\n", mmusic);
+			CONS_Alert(CONS_ERROR, "Music %.6s could not be loaded!\n", newmusic);
 			return;
 		}
 
 		music_flags = mflags;
 		music_looping = looping;
 
-		if (!S_PlayMusic(looping))
-		{
-			CONS_Alert(CONS_ERROR, "Music %.6s could not be played!\n", mmusic);
+		if (!S_PlayMusic(looping, fadeinms))
+ 		{
+			CONS_Alert(CONS_ERROR, "Music %.6s could not be played!\n", newmusic);
 			return;
 		}
+
+		if (position)
+			I_SetSongPosition(position);
+
+		I_SetSongTrack(mflags & MUSIC_TRACKMASK);
+	}
+	else if (fadeinms) // let fades happen with same music
+	{
+		I_SetSongPosition(position);
+		I_FadeSong(100, fadeinms, NULL);
+ 	}
+	else // reset volume to 100 with same music
+	{
+		I_StopFadingSong();
+		I_FadeSong(100, 500, NULL);
 	}
-	I_SetSongTrack(mflags & MUSIC_TRACKMASK);
 }
 
 void S_StopMusic(void)
@@ -1636,6 +1736,32 @@ void S_SetMusicVolume(INT32 digvolume, INT32 seqvolume)
 	}
 }
 
+/// ------------------------
+/// Music Fading
+/// ------------------------
+
+void S_SetInternalMusicVolume(INT32 volume)
+{
+	I_SetInternalMusicVolume(min(max(volume, 0), 100));
+}
+
+void S_StopFadingMusic(void)
+{
+	I_StopFadingSong();
+}
+
+boolean S_FadeMusicFromVolume(UINT8 target_volume, INT16 source_volume, UINT32 ms)
+{
+	if (source_volume < 0)
+		return I_FadeSong(target_volume, ms, NULL);
+	else
+		return I_FadeSongFromVolume(target_volume, source_volume, ms, NULL);
+}
+
+boolean S_FadeOutStopMusic(UINT32 ms)
+{
+	return I_FadeSong(0, ms, &S_StopMusic);
+}
 
 /// ------------------------
 /// Init & Others
@@ -1653,22 +1779,24 @@ void S_Start(void)
 		strncpy(mapmusname, mapheaderinfo[gamemap-1]->musname, 7);
 		mapmusname[6] = 0;
 		mapmusflags = (mapheaderinfo[gamemap-1]->mustrack & MUSIC_TRACKMASK);
+		mapmusposition = mapheaderinfo[gamemap-1]->muspos;
 	}
 
 	if (cv_resetmusic.value)
 		S_StopMusic();
-	S_ChangeMusic(mapmusname, mapmusflags, true);
+	S_ChangeMusicEx(mapmusname, mapmusflags, true, mapmusposition, 0, 0);
 }
 
 static void Command_Tunes_f(void)
 {
 	const char *tunearg;
 	UINT16 tunenum, track = 0;
+	UINT32 position = 0;
 	const size_t argc = COM_Argc();
 
 	if (argc < 2) //tunes slot ...
 	{
-		CONS_Printf("tunes <name/num> [track] [speed] / <-show> / <-default> / <-none>:\n");
+		CONS_Printf("tunes <name/num> [track] [speed] [position] / <-show> / <-default> / <-none>:\n");
 		CONS_Printf(M_GetText("Play an arbitrary music lump. If a map number is used, 'MAP##M' is played.\n"));
 		CONS_Printf(M_GetText("If the format supports multiple songs, you can specify which one to play.\n\n"));
 		CONS_Printf(M_GetText("* With \"-show\", shows the currently playing tune and track.\n"));
@@ -1715,10 +1843,15 @@ static void Command_Tunes_f(void)
 		snprintf(mapmusname, 7, "%sM", G_BuildMapName(tunenum));
 	else
 		strncpy(mapmusname, tunearg, 7);
+
+	if (argc > 4)
+		position = (UINT32)atoi(COM_Argv(4));
+
 	mapmusname[6] = 0;
 	mapmusflags = (track & MUSIC_TRACKMASK);
+	mapmusposition = position;
 
-	S_ChangeMusic(mapmusname, mapmusflags, true);
+	S_ChangeMusicEx(mapmusname, mapmusflags, true, mapmusposition, 0, 0);
 
 	if (argc > 3)
 	{
diff --git a/src/s_sound.h b/src/s_sound.h
index 8c03820f1b623fb66e186c2bf849e85e638aa6d9..3550ed0bdd574076a4b7a610363f3f3a07a4d10f 100644
--- a/src/s_sound.h
+++ b/src/s_sound.h
@@ -140,29 +140,49 @@ boolean S_MusicDisabled(void);
 boolean S_MusicPlaying(void);
 boolean S_MusicPaused(void);
 musictype_t S_MusicType(void);
+const char *S_MusicName(void);
 boolean S_MusicInfo(char *mname, UINT16 *mflags, boolean *looping);
 boolean S_MusicExists(const char *mname, boolean checkMIDI, boolean checkDigi);
 #define S_DigExists(a) S_MusicExists(a, false, true)
 #define S_MIDIExists(a) S_MusicExists(a, true, false)
 
-
 //
-// Music Properties
+// Music Effects
 //
 
 // Set Speed of Music
 boolean S_SpeedMusic(float speed);
 
 //
-// Music Routines
+// Music Seeking
+//
+
+// Get Length of Music
+UINT32 S_GetMusicLength(void);
+
+// Set LoopPoint of Music
+boolean S_SetMusicLoopPoint(UINT32 looppoint);
+
+// Get LoopPoint of Music
+UINT32 S_GetMusicLoopPoint(void);
+
+// Set Position of Music
+boolean S_SetMusicPosition(UINT32 position);
+
+// Get Position of Music
+UINT32 S_GetMusicPosition(void);
+
+//
+// Music Playback
 //
 
 // Start music track, arbitrary, given its name, and set whether looping
 // note: music flags 12 bits for tracknum (gme, other formats with more than one track)
 //       13-15 aren't used yet
 //       and the last bit we ignore (internal game flag for resetting music on reload)
-#define S_ChangeMusicInternal(a,b) S_ChangeMusic(a,0,b)
-void S_ChangeMusic(const char *mmusic, UINT16 mflags, boolean looping);
+void S_ChangeMusicEx(const char *mmusic, UINT16 mflags, boolean looping, UINT32 position, UINT32 prefadems, UINT32 fadeinms);
+#define S_ChangeMusicInternal(a,b) S_ChangeMusicEx(a,0,b,0,0,0)
+#define S_ChangeMusic(a,b,c) S_ChangeMusicEx(a,b,c,0,0,0)
 
 // Stops the music.
 void S_StopMusic(void);
@@ -171,6 +191,17 @@ void S_StopMusic(void);
 void S_PauseAudio(void);
 void S_ResumeAudio(void);
 
+//
+// Music Fading
+//
+
+void S_SetInternalMusicVolume(INT32 volume);
+void S_StopFadingMusic(void);
+boolean S_FadeMusicFromVolume(UINT8 target_volume, INT16 source_volume, UINT32 ms);
+#define S_FadeMusic(a, b) S_FadeMusicFromVolume(a, -1, b)
+#define S_FadeInChangeMusic(a,b,c,d) S_ChangeMusicEx(a,b,c,0,0,d)
+boolean S_FadeOutStopMusic(UINT32 ms);
+
 //
 // Updates music & sounds
 //
diff --git a/src/sdl/mixer_sound.c b/src/sdl/mixer_sound.c
index 6e90a6e06d323966cd45cae4d3a58973288ee06a..dde62fc7a2f69a28a96e71c5916168d6037d87c9 100644
--- a/src/sdl/mixer_sound.c
+++ b/src/sdl/mixer_sound.c
@@ -75,15 +75,41 @@
 UINT8 sound_started = false;
 
 static Mix_Music *music;
-static UINT8 music_volume, sfx_volume;
+static UINT8 music_volume, sfx_volume, internal_volume;
 static float loop_point;
+static float song_length; // length in seconds
 static boolean songpaused;
+static UINT32 music_bytes;
+static boolean is_looping;
+
+// fading
+static boolean is_fading;
+static UINT8 fading_source;
+static UINT8 fading_target;
+static UINT32 fading_timer;
+static UINT32 fading_duration;
+static INT32 fading_id;
+static void (*fading_callback)(void);
 
 #ifdef HAVE_LIBGME
 static Music_Emu *gme;
 static INT32 current_track;
 #endif
 
+static void var_cleanup(void)
+{
+	loop_point = song_length =\
+	 music_bytes = fading_source = fading_target =\
+	 fading_timer = fading_duration = 0;
+
+	songpaused = is_looping =\
+	 is_fading = false;
+
+	fading_callback = NULL;
+
+	internal_volume = 100;
+}
+
 /// ------------------------
 /// Audio System
 /// ------------------------
@@ -111,6 +137,8 @@ void I_StartupSound(void)
 		return;
 	}
 
+	var_cleanup();
+
 	music = NULL;
 	music_volume = sfx_volume = 0;
 
@@ -336,6 +364,7 @@ void *I_GetSfx(sfxinfo_t *sfx)
 					len = (info->play_length * 441 / 10) << 2;
 					mem = malloc(len);
 					gme_play(emu, len >> 1, mem);
+					gme_free_info(info);
 					gme_delete(emu);
 
 					return Mix_QuickLoad_RAW((Uint8 *)mem, len);
@@ -408,6 +437,7 @@ void *I_GetSfx(sfxinfo_t *sfx)
 		len = (info->play_length * 441 / 10) << 2;
 		mem = malloc(len);
 		gme_play(emu, len >> 1, mem);
+		gme_free_info(info);
 		gme_delete(emu);
 
 		return Mix_QuickLoad_RAW((Uint8 *)mem, len);
@@ -482,14 +512,102 @@ void I_SetSfxVolume(UINT8 volume)
 	sfx_volume = volume;
 }
 
+/// ------------------------
+/// Music Utilities
+/// ------------------------
+
+static UINT32 get_real_volume(UINT8 volume)
+{
+#ifdef _WIN32
+	if (I_SongType() == MU_MID)
+		// HACK: Until we stop using native MIDI,
+		// disable volume changes
+		return ((UINT32)31*128/31); // volume = 31
+	else
+#endif
+		// convert volume to mixer's 128 scale
+		// then apply internal_volume as a percentage
+		return ((UINT32)volume*128/31) * (UINT32)internal_volume / 100;
+}
+
+static UINT32 get_adjusted_position(UINT32 position)
+{
+	// all in milliseconds
+	UINT32 length = I_GetSongLength();
+	UINT32 looppoint = I_GetSongLoopPoint();
+	if (length)
+		return position >= length ? (position % (length-looppoint)) : position;
+	else
+		return position;
+}
+
+static void do_fading_callback(void)
+{
+	if (fading_callback)
+		(*fading_callback)();
+	fading_callback = NULL;
+}
+
 /// ------------------------
 /// Music Hooks
 /// ------------------------
 
+static void count_music_bytes(int chan, void *stream, int len, void *udata)
+{
+	(void)chan;
+	(void)stream;
+	(void)udata;
+
+	if (!music || I_SongType() == MU_GME || I_SongType() == MU_MOD || I_SongType() == MU_MID)
+		return;
+	music_bytes += len;
+}
+
 static void music_loop(void)
 {
-	Mix_PlayMusic(music, 0);
-	Mix_SetMusicPosition(loop_point);
+	if (is_looping)
+	{
+		Mix_PlayMusic(music, 0);
+		Mix_SetMusicPosition(loop_point);
+		music_bytes = loop_point*44100.0L*4; //assume 44.1khz, 4-byte length (see I_GetSongPosition)
+	}
+	else
+		I_StopSong();
+}
+
+static UINT32 music_fade(UINT32 interval, void *param)
+{
+	(void)param;
+
+	if (!is_fading ||
+		internal_volume == fading_target ||
+		fading_duration == 0)
+	{
+		I_StopFadingSong();
+		do_fading_callback();
+		return 0;
+	}
+	else if (songpaused) // don't decrement timer
+		return interval;
+	else if ((fading_timer -= 10) <= 0)
+	{
+		internal_volume = fading_target;
+		Mix_VolumeMusic(get_real_volume(music_volume));
+		I_StopFadingSong();
+		do_fading_callback();
+		return 0;
+	}
+	else
+	{
+		UINT8 delta = abs(fading_target - fading_source);
+		fixed_t factor = FixedDiv(fading_duration - fading_timer, fading_duration);
+		if (fading_target < fading_source)
+			internal_volume = max(min(internal_volume, fading_source - FixedMul(delta, factor)), fading_target);
+		else if (fading_target > fading_source)
+			internal_volume = min(max(internal_volume, fading_source + FixedMul(delta, factor)), fading_target);
+		Mix_VolumeMusic(get_real_volume(music_volume));
+		return interval;
+	}
 }
 
 #ifdef HAVE_LIBGME
@@ -509,7 +627,7 @@ static void mix_gme(void *udata, Uint8 *stream, int len)
 
 	// apply volume to stream
 	for (i = 0, p = (short *)stream; i < len/2; i++, p++)
-		*p = ((INT32)*p) * music_volume*2 / 42;
+		*p = ((INT32)*p) * (music_volume*internal_volume/100)*2 / 42;
 }
 #endif
 
@@ -587,6 +705,194 @@ boolean I_SetSongSpeed(float speed)
 	return false;
 }
 
+/// ------------------------
+///  MUSIC SEEKING
+/// ------------------------
+
+UINT32 I_GetSongLength(void)
+{
+	INT32 length;
+
+#ifdef HAVE_LIBGME
+	if (gme)
+	{
+		gme_info_t *info;
+		gme_err_t gme_e = gme_track_info(gme, &info, current_track);
+
+		if (gme_e != NULL)
+		{
+			CONS_Alert(CONS_ERROR, "GME error: %s\n", gme_e);
+			length = 0;
+		}
+		else
+		{
+			// reconstruct info->play_length, from GME source
+			// we only want intro + 1 loop, not 2
+			length = info->length;
+			if (length <= 0)
+			{
+				length = info->intro_length + info->loop_length; // intro + 1 loop
+				if (length <= 0)
+					length = 150 * 1000; // 2.5 minutes
+			}
+		}
+
+		gme_free_info(info);
+		return max(length, 0);
+	}
+	else
+#endif
+	if (!music || I_SongType() == MU_MOD || I_SongType() == MU_MID)
+		return 0;
+	else
+	{
+		// VERY IMPORTANT to set your LENGTHMS= in your song files, folks!
+		// SDL mixer can't read music length itself.
+		length = (UINT32)(song_length*1000);
+		if (!length)
+			CONS_Debug(DBG_DETAILED, "Getting music length: music is missing LENGTHMS= tag. Needed for seeking.\n");
+		return length;
+	}
+}
+
+boolean I_SetSongLoopPoint(UINT32 looppoint)
+{
+	if (!music || I_SongType() == MU_GME || I_SongType() == MU_MOD || I_SongType() == MU_MID || !is_looping)
+		return false;
+	else
+	{
+		UINT32 length = I_GetSongLength();
+
+		if (length > 0)
+			looppoint %= length;
+
+		loop_point = max((float)(looppoint / 1000.0L), 0);
+		return true;
+	}
+}
+
+UINT32 I_GetSongLoopPoint(void)
+{
+#ifdef HAVE_LIBGME
+	if (gme)
+	{
+		INT32 looppoint;
+		gme_info_t *info;
+		gme_err_t gme_e = gme_track_info(gme, &info, current_track);
+
+		if (gme_e != NULL)
+		{
+			CONS_Alert(CONS_ERROR, "GME error: %s\n", gme_e);
+			looppoint = 0;
+		}
+		else
+			looppoint = info->intro_length > 0 ? info->intro_length : 0;
+
+		gme_free_info(info);
+		return max(looppoint, 0);
+	}
+	else
+#endif
+	if (!music || I_SongType() == MU_MOD || I_SongType() == MU_MID)
+		return 0;
+	else
+		return (UINT32)(loop_point * 1000);
+}
+
+boolean I_SetSongPosition(UINT32 position)
+{
+	UINT32 length;
+#ifdef HAVE_LIBGME
+	if (gme)
+	{
+		// this is unstable, so fail silently
+		return true;
+		// this isn't required technically, but GME thread-locks for a second
+		// if you seek too high from the counter
+		// length = I_GetSongLength();
+		// if (length)
+		// 	position = get_adjusted_position(position);
+
+		// SDL_LockAudio();
+		// gme_err_t gme_e = gme_seek(gme, position);
+		// SDL_UnlockAudio();
+
+		// if (gme_e != NULL)
+		// {
+		// 	CONS_Alert(CONS_ERROR, "GME error: %s\n", gme_e);
+		// 	return false;
+		// }
+		// else
+		// 	return true;
+	}
+	else
+#endif
+	if (!music || I_SongType() == MU_MID)
+		return false;
+	else if (I_SongType() == MU_MOD)
+		return Mix_SetMusicPosition(position); // Goes by channels
+	else
+	{
+		// Because SDL mixer can't identify song length, if you have
+		// a position input greater than the real length, then
+		// music_bytes becomes inaccurate.
+
+		length = I_GetSongLength(); // get it in MS
+		if (length)
+			position = get_adjusted_position(position);
+
+		Mix_RewindMusic(); // needed for mp3
+		if(Mix_SetMusicPosition((float)(position/1000.0L)) == 0)
+			music_bytes = position/1000.0L*44100.0L*4; //assume 44.1khz, 4-byte length (see I_GetSongPosition)
+		else
+			// NOTE: This block fires on incorrect song format,
+			// NOT if position input is greater than song length.
+			music_bytes = 0;
+
+		return true;
+	}
+}
+
+UINT32 I_GetSongPosition(void)
+{
+#ifdef HAVE_LIBGME
+	if (gme)
+	{
+		INT32 position = gme_tell(gme);
+
+		gme_info_t *info;
+		gme_err_t gme_e = gme_track_info(gme, &info, current_track);
+
+		if (gme_e != NULL)
+		{
+			CONS_Alert(CONS_ERROR, "GME error: %s\n", gme_e);
+			return position;
+		}
+		else
+		{
+			// adjust position, since GME's counter keeps going past loop
+			if (info->length > 0)
+				position %= info->length;
+			else if (info->intro_length + info->loop_length > 0)
+				position = position >= (info->intro_length + info->loop_length) ? (position % info->loop_length) : position;
+			else
+				position %= 150 * 1000; // 2.5 minutes
+		}
+
+		gme_free_info(info);
+		return max(position, 0);
+	}
+	else
+#endif
+	if (!music || I_SongType() == MU_MID)
+		return 0;
+	else
+		return music_bytes/44100.0L*1000.0L/4; //assume 44.1khz
+		// 4 = byte length for 16-bit samples (AUDIO_S16SYS), stereo (2-channel)
+		// This is hardcoded in I_StartupSound. Other formats for factor:
+		// 8M: 1 | 8S: 2 | 16M: 2 | 16S: 4
+}
+
 /// ------------------------
 /// Music Playback
 /// ------------------------
@@ -596,9 +902,21 @@ boolean I_LoadSong(char *data, size_t len)
 	const char *key1 = "LOOP";
 	const char *key2 = "POINT=";
 	const char *key3 = "MS=";
+	const char *key4 = "LENGTHMS=";
 	const size_t key1len = strlen(key1);
 	const size_t key2len = strlen(key2);
 	const size_t key3len = strlen(key3);
+	const size_t key4len = strlen(key4);
+
+	// for mp3 wide chars
+	const char *key1w = "L\0O\0O\0P\0";
+	const char *key2w = "P\0O\0I\0N\0T\0\0\0\xFF\xFE";
+	const char *key3w = "M\0S\0\0\0\xFF\xFE";
+	const char *key4w = "L\0E\0N\0G\0T\0H\0M\0S\0\0\0\xFF\xFE";
+	const char *wterm = "\0\0";
+	char wval[10];
+
+	size_t wstart, wp;
 	char *p = data;
 	SDL_RWops *rw;
 
@@ -609,6 +927,9 @@ boolean I_LoadSong(char *data, size_t len)
 	)
 		I_UnloadSong();
 
+	// always do this whether or not a music already exists
+	var_cleanup();
+
 #ifdef HAVE_LIBGME
 	if ((UINT8)data[0] == 0x1F
 		&& (UINT8)data[1] == 0x8B)
@@ -718,30 +1039,88 @@ boolean I_LoadSong(char *data, size_t len)
 
 	// Find the OGG loop point.
 	loop_point = 0.0f;
+	song_length = 0.0f;
 
 	while ((UINT32)(p - data) < len)
 	{
-		if (strncmp(p++, key1, key1len))
-			continue;
-		p += key1len-1; // skip OOP (the L was skipped in strncmp)
-		if (!strncmp(p, key2, key2len)) // is it LOOPPOINT=?
+		if (fpclassify(loop_point) == FP_ZERO && !strncmp(p, key1, key1len))
 		{
-			p += key2len; // skip POINT=
-			loop_point = (float)((44.1L+atoi(p)) / 44100.0L); // LOOPPOINT works by sample count.
-			// because SDL_Mixer is USELESS and can't even tell us
-			// something simple like the frequency of the streaming music,
-			// we are unfortunately forced to assume that ALL MUSIC is 44100hz.
-			// This means a lot of tracks that are only 22050hz for a reasonable downloadable file size will loop VERY badly.
+			p += key1len; // skip LOOP
+			if (!strncmp(p, key2, key2len)) // is it LOOPPOINT=?
+			{
+				p += key2len; // skip POINT=
+				loop_point = (float)((44.1L+atoi(p)) / 44100.0L); // LOOPPOINT works by sample count.
+				// because SDL_Mixer is USELESS and can't even tell us
+				// something simple like the frequency of the streaming music,
+				// we are unfortunately forced to assume that ALL MUSIC is 44100hz.
+				// This means a lot of tracks that are only 22050hz for a reasonable downloadable file size will loop VERY badly.
+			}
+			else if (!strncmp(p, key3, key3len)) // is it LOOPMS=?
+			{
+				p += key3len; // skip MS=
+				loop_point = (float)(atoi(p) / 1000.0L); // LOOPMS works by real time, as miliseconds.
+				// Everything that uses LOOPMS will work perfectly with SDL_Mixer.
+			}
 		}
-		else if (!strncmp(p, key3, key3len)) // is it LOOPMS=?
+		else if (fpclassify(song_length) == FP_ZERO && !strncmp(p, key4, key4len)) // is it LENGTHMS=?
 		{
-			p += key3len; // skip MS=
-			loop_point = (float)(atoi(p) / 1000.0L); // LOOPMS works by real time, as miliseconds.
-			// Everything that uses LOOPMS will work perfectly with SDL_Mixer.
+			p += key4len; // skip LENGTHMS
+			song_length = (float)(atoi(p) / 1000.0L);
+		}
+		// below: search MP3 or other tags that use wide char encoding
+		else if (fpclassify(loop_point) == FP_ZERO && !memcmp(p, key1w, key1len*2)) // LOOP wide char
+		{
+			p += key1len*2;
+			if (!memcmp(p, key2w, (key2len+1)*2)) // POINT= wide char
+			{
+				p += (key2len+1)*2;
+				wstart = (size_t)p;
+				wp = 0;
+				while (wp < 9 && memcmp(p, wterm, 2))
+				{
+					wval[wp] = *p;
+					p += 2;
+					wp = ((size_t)(p-wstart))/2;
+				}
+				wval[min(wp, 9)] = 0;
+				loop_point = (float)((44.1L+atoi(wval) / 44100.0L));
+			}
+			else if (!memcmp(p, key3w, (key3len+1)*2)) // MS= wide char
+			{
+				p += (key3len+1)*2;
+				wstart = (size_t)p;
+				wp = 0;
+				while (wp < 9 && memcmp(p, wterm, 2))
+				{
+					wval[wp] = *p;
+					p += 2;
+					wp = ((size_t)(p-wstart))/2;
+				}
+				wval[min(wp, 9)] = 0;
+				loop_point = (float)(atoi(wval) / 1000.0L);
+			}
+		}
+		else if (fpclassify(song_length) == FP_ZERO && !memcmp(p, key4w, (key4len+1)*2)) // LENGTHMS= wide char
+		{
+			p += (key4len+1)*2;
+			wstart = (size_t)p;
+			wp = 0;
+			while (wp < 9 && memcmp(p, wterm, 2))
+			{
+				wval[wp] = *p;
+				p += 2;
+				wp = ((size_t)(p-wstart))/2;
+			}
+			wval[min(wp, 9)] = 0;
+			song_length = (float)(atoi(wval) / 1000.0L);
 		}
-		// Neither?! Continue searching.
-	}
 
+		if (fpclassify(loop_point) != FP_ZERO && fpclassify(song_length) != FP_ZERO && song_length > loop_point) // Got what we needed
+			// the last case is a sanity check, in case the wide char searches were false matches.
+			break;
+		else // continue searching
+			p++;
+	}
 	return true;
 }
 
@@ -765,7 +1144,6 @@ void I_UnloadSong(void)
 
 boolean I_PlaySong(boolean looping)
 {
-	boolean lpz = fpclassify(loop_point) == FP_ZERO;
 #ifdef HAVE_LIBGME
 	if (gme)
 	{
@@ -779,21 +1157,37 @@ boolean I_PlaySong(boolean looping)
 	if (!music)
 		return false;
 
+	if (fpclassify(song_length) == FP_ZERO && (I_SongType() == MU_OGG || I_SongType() == MU_MP3 || I_SongType() == MU_FLAC))
+		CONS_Debug(DBG_DETAILED, "This song is missing a LENGTHMS= tag! Required to make seeking work properly.\n");
 
-	if (Mix_PlayMusic(music, looping && lpz ? -1 : 0) == -1)
+	if (I_SongType() != MU_MOD && I_SongType() != MU_MID && Mix_PlayMusic(music, 0) == -1)
 	{
 		CONS_Alert(CONS_ERROR, "Mix_PlayMusic: %s\n", Mix_GetError());
 		return false;
 	}
-	Mix_VolumeMusic((UINT32)music_volume*128/31);
+	else if ((I_SongType() == MU_MOD || I_SongType() == MU_MID) && Mix_PlayMusic(music, looping ? -1 : 0) == -1) // if MOD, loop forever
+	{
+		CONS_Alert(CONS_ERROR, "Mix_PlayMusic: %s\n", Mix_GetError());
+		return false;
+	}
+
+	is_looping = looping;
+
+	I_SetMusicVolume(music_volume);
+
+	if (I_SongType() != MU_MOD && I_SongType() != MU_MID)
+		Mix_HookMusicFinished(music_loop); // don't bother counting if MOD
+
+	if(I_SongType() != MU_MOD && I_SongType() != MU_MID && !Mix_RegisterEffect(MIX_CHANNEL_POST, count_music_bytes, NULL, NULL))
+		CONS_Alert(CONS_WARNING, "Error registering SDL music position counter: %s\n", Mix_GetError());
 
-	if (!lpz)
-		Mix_HookMusicFinished(music_loop);
 	return true;
 }
 
 void I_StopSong(void)
 {
+	I_StopFadingSong();
+
 #ifdef HAVE_LIBGME
 	if (gme)
 	{
@@ -803,19 +1197,40 @@ void I_StopSong(void)
 #endif
 	if (music)
 	{
+		Mix_UnregisterEffect(MIX_CHANNEL_POST, count_music_bytes);
 		Mix_HookMusicFinished(NULL);
 		Mix_HaltMusic();
 	}
+
+	var_cleanup();
 }
 
 void I_PauseSong(void)
 {
+	if(I_SongType() == MU_MID) // really, SDL Mixer? why can't you pause MIDI???
+		return;
+
+	if(I_SongType() != MU_GME && I_SongType() != MU_MOD && I_SongType() != MU_MID)
+		Mix_UnregisterEffect(MIX_CHANNEL_POST, count_music_bytes);
+
 	Mix_PauseMusic();
 	songpaused = true;
 }
 
 void I_ResumeSong(void)
 {
+	if (I_SongType() == MU_MID)
+		return;
+
+	if (I_SongType() != MU_GME && I_SongType() != MU_MOD && I_SongType() != MU_MID)
+	{
+		while(Mix_UnregisterEffect(MIX_CHANNEL_POST, count_music_bytes) != 0) { }
+			// HACK: fixes issue of multiple effect callbacks being registered
+
+		if(music && I_SongType() != MU_MOD && I_SongType() != MU_MID && !Mix_RegisterEffect(MIX_CHANNEL_POST, count_music_bytes, NULL, NULL))
+			CONS_Alert(CONS_WARNING, "Error registering SDL music position counter: %s\n", Mix_GetError());
+	}
+
 	Mix_ResumeMusic();
 	songpaused = false;
 }
@@ -834,7 +1249,7 @@ void I_SetMusicVolume(UINT8 volume)
 #endif
 		music_volume = volume;
 
-	Mix_VolumeMusic((UINT32)music_volume*128/31);
+	Mix_VolumeMusic(get_real_volume(music_volume));
 }
 
 boolean I_SetSongTrack(int track)
@@ -863,9 +1278,100 @@ boolean I_SetSongTrack(int track)
 		SDL_UnlockAudio();
 		return false;
 	}
+	else
 #endif
+	if (I_SongType() == MU_MOD)
+		return !Mix_SetMusicPosition(track);
 	(void)track;
 	return false;
 }
 
+/// ------------------------
+/// MUSIC FADING
+/// ------------------------
+
+void I_SetInternalMusicVolume(UINT8 volume)
+{
+	internal_volume = volume;
+	if (!I_SongPlaying())
+		return;
+	Mix_VolumeMusic(get_real_volume(music_volume));
+}
+
+void I_StopFadingSong(void)
+{
+	if (fading_id)
+		SDL_RemoveTimer(fading_id);
+	is_fading = false;
+	fading_source = fading_target = fading_timer = fading_duration = fading_id = 0;
+}
+
+boolean I_FadeSongFromVolume(UINT8 target_volume, UINT8 source_volume, UINT32 ms, void (*callback)(void))
+{
+	INT16 volume_delta;
+
+	source_volume = min(source_volume, 100);
+	volume_delta = (INT16)(target_volume - source_volume);
+
+	I_StopFadingSong();
+
+	if (!ms && volume_delta)
+	{
+		I_SetInternalMusicVolume(target_volume);
+		if (callback)
+			(*callback)();
+		return true;
+
+	}
+	else if (!volume_delta)
+	{
+		if (callback)
+			(*callback)();
+		return true;
+	}
+
+	// Round MS to nearest 10
+	// If n - lower > higher - n, then round up
+	ms = (ms - ((ms / 10) * 10) > (((ms / 10) * 10) + 10) - ms) ?
+		(((ms / 10) * 10) + 10) // higher
+		: ((ms / 10) * 10); // lower
+
+	if (!ms)
+		I_SetInternalMusicVolume(target_volume);
+	else if (source_volume != target_volume)
+	{
+		fading_id = SDL_AddTimer(10, music_fade, NULL);
+		if (fading_id)
+		{
+			is_fading = true;
+			fading_timer = fading_duration = ms;
+			fading_source = source_volume;
+			fading_target = target_volume;
+			fading_callback = callback;
+
+			if (internal_volume != source_volume)
+				I_SetInternalMusicVolume(source_volume);
+		}
+	}
+
+	return is_fading;
+}
+
+boolean I_FadeSong(UINT8 target_volume, UINT32 ms, void (*callback)(void))
+{
+	return I_FadeSongFromVolume(target_volume, internal_volume, ms, callback);
+}
+
+boolean I_FadeOutStopSong(UINT32 ms)
+{
+	return I_FadeSongFromVolume(0, internal_volume, ms, &I_StopSong);
+}
+
+boolean I_FadeInPlaySong(UINT32 ms, boolean looping)
+{
+	if (I_PlaySong(looping))
+		return I_FadeSongFromVolume(100, 0, ms, NULL);
+	else
+		return false;
+}
 #endif
diff --git a/src/sdl/sdl_sound.c b/src/sdl/sdl_sound.c
index 9ff1dd0b2bda00e0e8154a46aa4e8e6dd5d4bd7d..d9967ae0375d9ab30a3772f9c934e5ee26c75663 100644
--- a/src/sdl/sdl_sound.c
+++ b/src/sdl/sdl_sound.c
@@ -1375,6 +1375,37 @@ boolean I_SetSongSpeed(float speed)
 	return false;
 }
 
+/// ------------------------
+//  MUSIC SEEKING
+/// ------------------------
+
+UINT32 I_GetSongLength(void)
+{
+	return 0;
+}
+
+boolean I_SetSongLoopPoint(UINT32 looppoint)
+{
+        (void)looppoint;
+        return false;
+}
+
+UINT32 I_GetSongLoopPoint(void)
+{
+	return 0;
+}
+
+boolean I_SetSongPosition(UINT32 position)
+{
+	(void)position;
+	return false;
+}
+
+UINT32 I_GetSongPosition(void)
+{
+	return 0;
+}
+
 /// ------------------------
 //  MUSIC PLAYBACK
 /// ------------------------
@@ -1443,6 +1474,47 @@ boolean I_SetSongTrack(int track)
 	return false;
 }
 
+/// ------------------------
+/// MUSIC FADING
+/// ------------------------
+
+void I_SetInternalMusicVolume(UINT8 volume)
+{
+	(void)volume;
+}
+
+void I_StopFadingSong(void)
+{
+}
+
+boolean I_FadeSongFromVolume(UINT8 target_volume, UINT8 source_volume, UINT32 ms, void (*callback)(void))
+{
+	(void)target_volume;
+	(void)source_volume;
+	(void)ms;
+	return false;
+}
+
+boolean I_FadeSong(UINT8 target_volume, UINT32 ms, void (*callback)(void))
+{
+	(void)target_volume;
+	(void)ms;
+	return false;
+}
+
+boolean I_FadeOutStopSong(UINT32 ms)
+{
+	(void)ms;
+	return false;
+}
+
+boolean I_FadeInPlaySong(UINT32 ms, boolean looping)
+{
+        (void)ms;
+        (void)looping;
+        return false;
+}
+
 /// ------------------------
 //  MUSIC LOADING AND CLEANUP
 //  \todo Split logic between loading and playing,
diff --git a/src/sounds.c b/src/sounds.c
index 7408dec42f2cbb357de293404f16f75032abc7cb..66d4988381019072d63d639c74a3333648914b00 100644
--- a/src/sounds.c
+++ b/src/sounds.c
@@ -229,6 +229,7 @@ sfxinfo_t S_sfx[NUMSFX] =
   {"hoop3",  false, 192,  0, -1, NULL, 0,        -1,  -1, LUMPERROR, "Hoop++"},
   {"hidden", false, 204,  0, -1, NULL, 0,        -1,  -1, LUMPERROR, "Discovery"},
   {"prloop", false, 104,  0, -1, NULL, 0,        -1,  -1, LUMPERROR, "Gust of wind"},
+  {"timeup",  true, 256,  0, -1, NULL, 0,        -1,  -1, LUMPERROR, "Ominous Countdown"},
   {"ngjump", false,  96,  0, -1, NULL, 0,        -1,  -1, LUMPERROR, "Jump"},
   {"peww",   false,  96,  0, -1, NULL, 0,        -1,  -1, LUMPERROR, "Pew"},
 
diff --git a/src/sounds.h b/src/sounds.h
index f4847b226e5e86dab8b63810cb874a5db3f26ab1..fd1142aba39ecae2846d47c2274648a9df3f6e1a 100644
--- a/src/sounds.h
+++ b/src/sounds.h
@@ -295,6 +295,7 @@ typedef enum
 	sfx_hoop3,
 	sfx_hidden,
 	sfx_prloop,
+	sfx_timeup,
 	sfx_ngjump,
 	sfx_peww,
 
diff --git a/src/win32/win_snd.c b/src/win32/win_snd.c
index 454c53e37ee7d5a1fa72bdebbeff9715f94855e6..f3e3bbed487132ef0421474f3dfc90ee70f0e84a 100644
--- a/src/win32/win_snd.c
+++ b/src/win32/win_snd.c
@@ -815,6 +815,60 @@ void I_SetMusicVolume(UINT8 volume)
 	FMR_MUSIC(FMOD_Channel_SetVolume(music_channel, music_volume / 31.0));
 }
 
+UINT32 I_GetSongLength(void)
+{
+	UINT32 length;
+	if (I_SongType() == MU_MID)
+		return 0;
+	FMR_MUSIC(FMOD_Sound_GetLength(music_stream, &length, FMOD_TIMEUNIT_MS));
+	return length;
+}
+
+boolean I_SetSongLoopPoint(UINT32 looppoint)
+{
+        (void)looppoint;
+        return false;
+}
+
+UINT32 I_GetSongLoopPoint(void)
+{
+	return 0;
+}
+
+boolean I_SetSongPosition(UINT32 position)
+{
+	FMOD_RESULT e;
+	if(I_SongType() == MU_MID)
+		// Dummy out; this works for some MIDI, but not others.
+		// SDL does not support this for any MIDI.
+		return false;
+	e = FMOD_Channel_SetPosition(music_channel, position, FMOD_TIMEUNIT_MS);
+	if (e == FMOD_OK)
+		return true;
+	else if (e == FMOD_ERR_UNSUPPORTED // Only music modules, numbnuts!
+			|| e == FMOD_ERR_INVALID_POSITION) // Out-of-bounds!
+		return false;
+	else // Congrats, you horribly broke it somehow
+	{
+		FMR_MUSIC(e);
+		return false;
+	}
+}
+
+UINT32 I_GetSongPosition(void)
+{
+	FMOD_RESULT e;
+	unsigned int fmposition = 0;
+	if(I_SongType() == MU_MID)
+		// Dummy out because unsupported, even though FMOD does this correctly.
+		return 0;
+	e = FMOD_Channel_GetPosition(music_channel, &fmposition, FMOD_TIMEUNIT_MS);
+	if (e == FMOD_OK)
+		return (UINT32)fmposition;
+	else
+		return 0;
+}
+
 boolean I_SetSongTrack(INT32 track)
 {
 	if (track != current_track) // If the track's already playing, then why bother?
@@ -859,3 +913,46 @@ boolean I_SetSongTrack(INT32 track)
 	}
 	return false;
 }
+
+/// ------------------------
+/// MUSIC FADING
+/// ------------------------
+
+void I_SetInternalMusicVolume(UINT8 volume)
+{
+	(void)volume;
+}
+
+void I_StopFadingSong(void)
+{
+}
+
+boolean I_FadeSongFromVolume(UINT8 target_volume, UINT8 source_volume, UINT32 ms, void (*callback)(void))
+{
+	(void)target_volume;
+	(void)source_volume;
+	(void)ms;
+	(void)callback;
+	return false;
+}
+
+boolean I_FadeSong(UINT8 target_volume, UINT32 ms, void (*callback)(void))
+{
+	(void)target_volume;
+	(void)ms;
+	(void)callback;
+	return false;
+}
+
+boolean I_FadeOutStopSong(UINT32 ms)
+{
+	(void)ms;
+	return false;
+}
+
+boolean I_FadeInPlaySong(UINT32 ms, boolean looping)
+{
+        (void)ms;
+        (void)looping;
+        return false;
+}
diff --git a/src/y_inter.c b/src/y_inter.c
index a36a9815de387f33416b636f1908dacbd09f7b60..99219396426e86ad8b40a3a088cabdf243f61a7d 100644
--- a/src/y_inter.c
+++ b/src/y_inter.c
@@ -833,7 +833,17 @@ void Y_Ticker(void)
 
 		if (!intertic) // first time only
 		{
-			S_ChangeMusicInternal("_clear", false); // don't loop it
+			if (mapheaderinfo[gamemap-1]->musinterfadeout
+#ifdef _WIN32
+				// can't fade midi due to win32 volume hack
+				&& S_MusicType() != MU_MID
+#endif
+			)
+				S_FadeOutStopMusic(mapheaderinfo[gamemap-1]->musinterfadeout);
+			else if (mapheaderinfo[gamemap-1]->musintername[0] && S_MusicExists(mapheaderinfo[gamemap-1]->musintername, !midi_disabled, !digital_disabled))
+				S_ChangeMusicInternal(mapheaderinfo[gamemap-1]->musintername, false); // don't loop it
+			else
+				S_ChangeMusicInternal("_clear", false); // don't loop it
 			tallydonetic = -1;
 		}
 
@@ -898,7 +908,17 @@ void Y_Ticker(void)
 
 		if (!intertic) // first time only
 		{
-			S_ChangeMusicInternal("_clear", false); // don't loop it
+			if (mapheaderinfo[gamemap-1]->musinterfadeout
+#ifdef _WIN32
+				// can't fade midi due to win32 volume hack
+				&& S_MusicType() != MU_MID
+#endif
+			)
+				S_FadeOutStopMusic(mapheaderinfo[gamemap-1]->musinterfadeout);
+			else if (mapheaderinfo[gamemap-1]->musintername[0] && S_MusicExists(mapheaderinfo[gamemap-1]->musintername, !midi_disabled, !digital_disabled))
+				S_ChangeMusicInternal(mapheaderinfo[gamemap-1]->musintername, false); // don't loop it
+			else
+				S_ChangeMusicInternal("_clear", false); // don't loop it
 			tallydonetic = -1;
 		}