From c60d61a493684bcb90cfd6ac77aa4506be132d05 Mon Sep 17 00:00:00 2001
From: mazmazz <mar.marcoz@outlook.com>
Date: Tue, 18 Sep 2018 15:10:00 -0400
Subject: [PATCH 01/10] musicplus-jingle 2.2 -> 2.1 backport

---
 src/dehacked.c       |  17 +++
 src/p_enemy.c        |   7 +-
 src/p_inter.c        |   7 +-
 src/p_local.h        |  40 +++++++
 src/p_setup.c        |  36 ++++--
 src/p_spec.c         |  58 ++++++++--
 src/p_tick.c         |   7 ++
 src/p_user.c         | 200 ++++++++++++++++++++++++++++-----
 src/s_sound.c        | 261 +++++++++++++++++++++++++++++++++++++++++++
 src/s_sound.h        |  24 ++++
 src/sdl/i_video.c    |   5 +-
 src/win32/win_main.c |   6 +-
 12 files changed, 605 insertions(+), 63 deletions(-)

diff --git a/src/dehacked.c b/src/dehacked.c
index e2df11142..e145e12aa 100644
--- a/src/dehacked.c
+++ b/src/dehacked.c
@@ -7190,6 +7190,23 @@ struct {
 	{"GT_HIDEANDSEEK",GT_HIDEANDSEEK},
 	{"GT_CTF",GT_CTF},
 
+	// Jingles (jingletype_t)
+	{"JT_NONE",JT_NONE},
+	{"JT_OTHER",JT_OTHER},
+	{"JT_MASTER",JT_MASTER},
+	{"JT_1UP",JT_1UP},
+	{"JT_SHOES",JT_SHOES},
+	{"JT_INV",JT_INV},
+	{"JT_MINV",JT_MINV},
+	{"JT_DROWN",JT_DROWN},
+	{"JT_SUPER",JT_SUPER},
+	{"JT_GOVER",JT_GOVER},
+	{"JT_NIGHTSTIMEOUT",JT_NIGHTSTIMEOUT},
+	{"JT_SSTIMEOUT",JT_SSTIMEOUT},
+	// {"JT_LCLEAR",JT_LCLEAR},
+	// {"JT_RACENT",JT_RACENT},
+	// {"JT_CONTSC",JT_CONTSC},
+
 	// Player state (playerstate_t)
 	{"PST_LIVE",PST_LIVE}, // Playing or camping.
 	{"PST_DEAD",PST_DEAD}, // Dead on the ground, view follows killer.
diff --git a/src/p_enemy.c b/src/p_enemy.c
index f2e54a583..11e3856c4 100644
--- a/src/p_enemy.c
+++ b/src/p_enemy.c
@@ -3056,10 +3056,10 @@ void A_Invincibility(mobj_t *actor)
 
 	if (P_IsLocalPlayer(player) && !player->powers[pw_super])
 	{
-		S_StopMusic();
 		if (mariomode)
 			G_GhostAddColor(GHC_INVINCIBLE);
 		S_ChangeMusicInternal((mariomode) ? "minvnc" : "invinc", false);
+		P_PlayJingle(player, (mariomode) ? JT_MINV : JT_INV);
 	}
 }
 
@@ -3093,10 +3093,7 @@ void A_SuperSneakers(mobj_t *actor)
 		if (S_SpeedMusic(0.0f) && (mapheaderinfo[gamemap-1]->levelflags & LF_SPEEDMUSIC))
 			S_SpeedMusic(1.4f);
 		else
-		{
-			S_StopMusic();
-			S_ChangeMusicInternal("shoes", false);
-		}
+			P_PlayJingle(player, JT_SHOES);
 	}
 }
 
diff --git a/src/p_inter.c b/src/p_inter.c
index 407e091fa..7a17fbcf6 100644
--- a/src/p_inter.c
+++ b/src/p_inter.c
@@ -2088,10 +2088,7 @@ void P_KillMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source)
 			if (target->player->lives <= 0) // Tails 03-14-2000
 			{
 				if (P_IsLocalPlayer(target->player)/* && target->player == &players[consoleplayer] */)
-				{
-					S_StopMusic(); // Stop the Music! Tails 03-14-2000
-					S_ChangeMusicInternal("gmover", false); // Yousa dead now, Okieday? Tails 03-14-2000
-				}
+					P_PlayJingle(target->player, JT_GOVER); // Yousa dead now, Okieday? Tails 03-14-2000
 			}
 		}
 		target->player->playerstate = PST_DEAD;
@@ -2466,7 +2463,7 @@ static inline void P_NiGHTSDamage(mobj_t *target, mobj_t *source)
 			&& 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("drown",false);
+			P_PlayJingle(player, ((maptol & TOL_NIGHTS) && !G_IsSpecialStage(gamemap)) ? JT_NIGHTSTIMEOUT : JT_SSTIMEOUT);
 		}
 	}
 }
diff --git a/src/p_local.h b/src/p_local.h
index 1fd7ada04..190bd7792 100644
--- a/src/p_local.h
+++ b/src/p_local.h
@@ -184,6 +184,46 @@ void P_PlayLivesJingle(player_t *player);
 #define P_PlayDeathSound(s)		S_StartSound(s, sfx_altdi1 + P_RandomKey(4));
 #define P_PlayVictorySound(s)	S_StartSound(s, sfx_victr1 + P_RandomKey(4));
 
+/// ------------------------
+/// Jingle stuff
+/// ------------------------
+
+typedef enum
+{
+	JT_NONE,   // Null state
+	JT_OTHER,  // Other state
+	JT_MASTER, // Main level music
+	JT_1UP, // Extra life
+	JT_SHOES,  // Speed shoes
+	JT_INV, // Invincibility
+	JT_MINV, // Mario Invincibility
+	JT_DROWN,  // Drowning
+	JT_SUPER,  // Super Sonic
+	JT_GOVER, // Game Over
+	JT_NIGHTSTIMEOUT, // NiGHTS Time Out (10 seconds)
+	JT_SSTIMEOUT, // NiGHTS Special Stage Time Out (10 seconds)
+
+	// these are not jingles
+	// JT_LCLEAR, // Level Clear
+	// JT_RACENT, // Multiplayer Intermission
+	// JT_CONTSC, // Continue
+
+	NUMJINGLES
+} jingletype_t;
+
+typedef struct
+{
+	char musname[7];
+	boolean looping;
+} jingle_t;
+
+extern jingle_t jingleinfo[NUMJINGLES];
+
+#define JINGLEPOSTFADE 1000
+
+void P_PlayJingle(player_t *player, jingletype_t jingletype);
+boolean P_EvaluateMusicStatus(UINT16 status);
+void P_PlayJingleMusic(player_t *player, const char *musname, UINT16 musflags, boolean looping, UINT16 status);
 
 //
 // P_MOBJ
diff --git a/src/p_setup.c b/src/p_setup.c
index 6c6b9153d..f504ffb57 100644
--- a/src/p_setup.c
+++ b/src/p_setup.c
@@ -1514,19 +1514,33 @@ static void P_LoadSideDefs2(lumpnum_t lumpnum)
 				{
 					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;
 			}
 			case 414: // Play SFX
diff --git a/src/p_spec.c b/src/p_spec.c
index afc3c0533..7832ff038 100644
--- a/src/p_spec.c
+++ b/src/p_spec.c
@@ -2419,18 +2419,60 @@ 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 : 0, -1);
 
-				strncpy(mapmusname, sides[line->sidenum[0]].text, 7);
-				mapmusname[6] = 0;
+				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);
+				}
 
-				mapmusflags = tracknum & MUSIC_TRACKMASK;
-				if (!(line->flags & ML_BLOCKMONSTERS))
-					mapmusflags |= MUSIC_RELOADRESET;
+				if ((line->flags & ML_EFFECT2) && fadetarget && musicsame)
+				{
+					if (!postfadems)
+						S_SetInternalMusicVolume(fadetarget);
+					else
+						S_FadeMusicFromVolume(fadetarget, fadesource, postfadems);
 
-				mapmusposition = 0;
+					if (position)
+						S_SetMusicPosition(position);
+				}
+				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_tick.c b/src/p_tick.c
index f4bc59323..7b1878d93 100644
--- a/src/p_tick.c
+++ b/src/p_tick.c
@@ -582,13 +582,20 @@ void P_Ticker(boolean run)
 			OP_ObjectplaceMovement(&players[0]);
 			P_MoveChaseCamera(&players[0], &camera, false);
 			P_MapEnd();
+			S_SetStackAdjustmentStart();
 			return;
 		}
 	}
 
 	// Check for pause or menu up in single player
 	if (paused || P_AutoPause())
+	{
+		S_SetStackAdjustmentStart();
 		return;
+	}
+
+	if (!S_MusicPaused())
+		S_AdjustMusicStackTics();
 
 	postimgtype = postimgtype2 = postimg_none;
 
diff --git a/src/p_user.c b/src/p_user.c
index 03b037fed..cb14814a2 100644
--- a/src/p_user.c
+++ b/src/p_user.c
@@ -56,6 +56,29 @@
 static void P_NukeAllPlayers(player_t *player);
 #endif
 
+//
+// Jingle stuff.
+//
+
+jingle_t jingleinfo[NUMJINGLES] = {
+	// {musname, looping, reset, nest}
+	{""        , false}, // JT_NONE
+	{""        , false}, // JT_OTHER
+	{""        , false}, // JT_MASTER
+	{"xtlife"    , false},
+	{"shoes"  ,  true},
+	{"invinc"    , false},
+	{"minvnc"   , false},
+	{"drown"  , false},
+	{"supers"  ,  true},
+	{"gmover"  , false},
+	{"drown"  , false},  // JT_NIGHTSTIMEOUT
+	{"drown"  , false}   // JT_SSTIMEOUT
+	// {"lclear"  , false},
+	// {"racent"  ,  true},
+	// {"contsc"  ,  true}
+};
+
 //
 // Movement.
 //
@@ -636,6 +659,7 @@ static void P_DeNightserizePlayer(player_t *player)
 	}
 
 	// Restore from drowning music
+	music_stack_fadein = 0; // HACK: Change fade-in for restore music
 	P_RestoreMusic(player);
 }
 //
@@ -675,6 +699,7 @@ void P_NightserizePlayer(player_t *player, INT32 nighttime)
 	player->nightstime = player->startedtime = nighttime*TICRATE;
 	player->bonustime = false;
 
+	music_stack_fadein = 0; // HACK: Change fade-in for restore music
 	P_RestoreMusic(player);
 	P_SetMobjState(player->mo->tracer, S_SUPERTRANS1);
 
@@ -961,8 +986,7 @@ void P_DoSuperTransformation(player_t *player, boolean giverings)
 	player->powers[pw_super] = 1;
 	if (!(mapheaderinfo[gamemap-1]->levelflags & LF_NOSSMUSIC) && P_IsLocalPlayer(player))
 	{
-		S_StopMusic();
-		S_ChangeMusicInternal("supers", true);
+		P_PlayJingle(player, JT_SUPER);
 	}
 
 	S_StartSound(NULL, sfx_supert); //let all players hear it -mattw_cfi
@@ -1097,11 +1121,111 @@ void P_PlayLivesJingle(player_t *player)
 	{
 		if (player)
 			player->powers[pw_extralife] = extralifetics + 1;
-		S_StopMusic(); // otherwise it won't restart if this is done twice in a row
-		S_ChangeMusicInternal("xtlife", false);
+		P_PlayJingle(player, JT_1UP);
 	}
 }
 
+void P_PlayJingle(player_t *player, jingletype_t jingletype)
+{
+	const char *musname = jingleinfo[jingletype].musname;
+	UINT16 musflags = 0;
+	boolean looping = jingleinfo[jingletype].looping;
+
+	char newmusic[7];
+	strncpy(newmusic, musname, 7);
+#if defined(HAVE_BLUA) && defined(HAVE_LUA_MUSICPLUS)
+ 	if(LUAh_MusicJingle(jingletype, newmusic, &musflags, &looping))
+ 		return;
+#endif
+	newmusic[6] = 0;
+
+	P_PlayJingleMusic(player, newmusic, musflags, looping, jingletype);
+}
+
+//
+// P_PlayJingleMusic
+//
+void P_PlayJingleMusic(player_t *player, const char *musname, UINT16 musflags, boolean looping, UINT16 status)
+{
+	if (!P_IsLocalPlayer(player))
+		return;
+
+	S_RetainMusic(musname, musflags, looping, 0, status);
+	S_StopMusic();
+	S_ChangeMusicInternal(musname, looping);
+}
+
+boolean P_EvaluateMusicStatus(UINT16 status)
+{
+	// \todo lua hook
+	int i;
+	boolean result = false;
+
+	for (i = 0; i < MAXPLAYERS; i++)
+	{
+		if (!P_IsLocalPlayer(&players[i]))
+			continue;
+
+		switch(status)
+		{
+			case JT_1UP: // Extra life
+				result = (players[i].powers[pw_extralife] > 1);
+				break;
+
+			case JT_SHOES:  // Speed shoes
+				if (players[i].powers[pw_sneakers] > 1 && !players[i].powers[pw_super])
+				{
+					//strlcpy(S_sfx[sfx_None].caption, "Speed shoes", 12);
+					//S_StartCaption(sfx_None, -1, players[i].powers[pw_sneakers]);
+					result = true;
+				}
+				else
+					result = false;
+				break;
+
+			case JT_INV: // Invincibility
+			case JT_MINV: // Mario Invincibility
+				if (players[i].powers[pw_invulnerability] > 1)
+				{
+					//strlcpy(S_sfx[sfx_None].caption, "Invincibility", 14);
+					//S_StartCaption(sfx_None, -1, players[i].powers[pw_invulnerability]);
+					result = true;
+				}
+				else
+					result = false;
+				break;
+
+			case JT_DROWN:  // Drowning
+				result = (players[i].powers[pw_underwater] && players[i].powers[pw_underwater] <= 11*TICRATE + 1);
+				break;
+
+			case JT_SUPER:  // Super Sonic
+				result = (players[i].powers[pw_super] && !(mapheaderinfo[gamemap-1]->levelflags & LF_NOSSMUSIC));
+				break;
+
+			case JT_GOVER: // Game Over
+				result = (players[i].lives <= 0);
+				break;
+
+			case JT_NIGHTSTIMEOUT: // NiGHTS Time Out (10 seconds)
+			case JT_SSTIMEOUT:
+				result = (players[i].nightstime && players[i].nightstime <= 10*TICRATE);
+				break;
+
+			case JT_NONE:   // Null state
+			case JT_OTHER:  // Other state
+			case JT_MASTER: // Main level music
+			default:
+				result = true;
+		}
+
+		if (result)
+			break;
+ 	}
+
+	return result;
+ }
+
 //
 // P_RestoreMusic
 //
@@ -1112,25 +1236,46 @@ void P_RestoreMusic(player_t *player)
 	if (!P_IsLocalPlayer(player)) // Only applies to a local player
 		return;
 
+	S_SpeedMusic(1.0f);
+
+	// Jingles have a priority in this order, so follow it
+	// and as a default case, go down the music stack.
+
+	// Extra life
 	if (player->powers[pw_extralife] > 1)
 		return;
-	S_SpeedMusic(1.0f);
-	if (player->powers[pw_super] && !(mapheaderinfo[gamemap-1]->levelflags & LF_NOSSMUSIC))
-		S_ChangeMusicInternal("supers", true);
+
+	// Super
+	else if (player->powers[pw_super] && !(mapheaderinfo[gamemap-1]->levelflags & LF_NOSSMUSIC)
+		&& !S_RecallMusic(JT_SUPER, false))
+		P_PlayJingle(player, JT_SUPER);
+
+	// Invulnerability
 	else if (player->powers[pw_invulnerability] > 1)
-		S_ChangeMusicInternal((mariomode) ? "minvnc" : "invinc", false);
+	{
+		if (!S_RecallMusic(JT_INV, false) && !S_RecallMusic(JT_MINV, false))
+			P_PlayJingle(player, (mariomode) ? JT_MINV : JT_INV);
+	}
+
+	// Shoes
 	else if (player->powers[pw_sneakers] > 1 && !player->powers[pw_super])
 	{
 		if (mapheaderinfo[gamemap-1]->levelflags & LF_SPEEDMUSIC)
 		{
 			S_SpeedMusic(1.4f);
-			S_ChangeMusicEx(mapmusname, mapmusflags, true, mapmusposition, 0, 0);
+			if (!S_RecallMusic(JT_MASTER, true))
+				S_ChangeMusicEx(mapmusname, mapmusflags, true, mapmusposition, 0, 0);
 		}
-		else
-			S_ChangeMusicInternal("shoes", true);
+		else if (!S_RecallMusic(JT_SHOES, false))
+			P_PlayJingle(player, JT_SHOES);
 	}
-	else
-		S_ChangeMusicEx(mapmusname, mapmusflags, true, mapmusposition, 0, 0);
+
+	// Default
+	else if (!S_RecallMusic(JT_NONE, false)) // go down the stack
+	{
+		CONS_Debug(DBG_BASIC, "Cannot find any music in resume stack!\n");
+	 	S_ChangeMusicEx(mapmusname, mapmusflags, true, mapmusposition, 0, 0);
+ 	}
 }
 
 //
@@ -2017,10 +2162,6 @@ static void P_CheckQuicksand(player_t *player)
 //
 static void P_CheckSneakerAndLivesTimer(player_t *player)
 {
-	if ((player->powers[pw_underwater] <= 11*TICRATE + 1)
-	&& (player->powers[pw_underwater] > 1))
-		return; // don't restore music if drowning music is playing
-
 	if (player->powers[pw_extralife] == 1) // Extra Life!
 		P_RestoreMusic(player);
 
@@ -2105,15 +2246,18 @@ static void P_CheckUnderwaterAndSpaceTimer(player_t *player)
 	if (!(player->mo->eflags & MFE_UNDERWATER) && player->powers[pw_underwater])
 	{
 		if (player->powers[pw_underwater] <= 12*TICRATE + 1)
+		{
+			player->powers[pw_underwater] = 0;
 			P_RestoreMusic(player);
-
-		player->powers[pw_underwater] = 0;
+		}
+		else
+			player->powers[pw_underwater] = 0;
 	}
 
 	if (player->powers[pw_spacetime] > 1 && !P_InSpaceSector(player->mo))
 	{
-		P_RestoreMusic(player);
 		player->powers[pw_spacetime] = 0;
+		P_RestoreMusic(player);
 	}
 
 	// Underwater audio cues
@@ -2122,8 +2266,7 @@ static void P_CheckUnderwaterAndSpaceTimer(player_t *player)
 		if (player->powers[pw_underwater] == 11*TICRATE + 1
 		&& player == &players[consoleplayer])
 		{
-			S_StopMusic();
-			S_ChangeMusicInternal("drown", false);
+			P_PlayJingle(player, JT_DROWN);
 		}
 
 		if (player->powers[pw_underwater] == 25*TICRATE + 1)
@@ -2185,10 +2328,6 @@ static void P_CheckInvincibilityTimer(player_t *player)
 			P_SpawnShieldOrb(player);
 		}
 
-		if ((player->powers[pw_underwater] <= 11*TICRATE + 1)
-		&& (player->powers[pw_underwater] > 1))
-			return; // don't restore music if drowning music is playing
-
 		if (!player->powers[pw_super] || (mapheaderinfo[gamemap-1]->levelflags & LF_NOSSMUSIC))
 			P_RestoreMusic(player);
 	}
@@ -3410,6 +3549,8 @@ static void P_DoSuperStuff(player_t *player)
 		{
 			player->powers[pw_super] = 0;
 			P_SetPlayerMobjState(player->mo, S_PLAY_STND);
+			music_stack_noposition = true; // HACK: Do not reposition next music
+			music_stack_fadeout = MUSICRATE/2; // HACK: Fade out current music
 			P_RestoreMusic(player);
 			P_SpawnShieldOrb(player);
 
@@ -3511,6 +3652,8 @@ static void P_DoSuperStuff(player_t *player)
 			}
 
 			// Resume normal music if you're the console player
+			music_stack_noposition = true; // HACK: Do not reposition next music
+			music_stack_fadeout = MUSICRATE/2; // HACK: Fade out current music
 			P_RestoreMusic(player);
 
 			// If you had a shield, restore its visual significance.
@@ -5576,13 +5719,14 @@ 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
+		music_stack_fadein = 0; // HACK: Change fade-in for restore music
 		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("drown",false);
+		P_PlayJingle(player, ((maptol & TOL_NIGHTS) && !G_IsSpecialStage(gamemap)) ? JT_NIGHTSTIMEOUT : JT_SSTIMEOUT);
 
 
 	if (player->mo->z < player->mo->floorz)
@@ -8754,7 +8898,7 @@ void P_PlayerThink(player_t *player)
 		if (countdown == 11*TICRATE - 1)
 		{
 			if (P_IsLocalPlayer(player))
-				S_ChangeMusicInternal("drown", false);
+				P_PlayJingle(player, JT_DROWN);
 		}
 
 		// If you've hit the countdown and you haven't made
diff --git a/src/s_sound.c b/src/s_sound.c
index 3da594fe1..bec6b1ba2 100644
--- a/src/s_sound.c
+++ b/src/s_sound.c
@@ -1229,6 +1229,10 @@ static boolean   queue_looping;
 static UINT32    queue_position;
 static UINT32    queue_fadeinms;
 
+static musicstack_t music_stack[NUMMUSICSTACKS];
+
+static tic_t     pause_starttic;
+
 /// ------------------------
 /// Music Status
 /// ------------------------
@@ -1327,6 +1331,254 @@ UINT32 S_GetMusicPosition(void)
 	return I_GetSongPosition();
 }
 
+/// ------------------------
+/// Music Stacking (Jingles)
+/// In this section: mazmazz doesn't know how to do dynamic arrays or struct pointers!
+/// ------------------------
+
+static const musicstack_t empty_music_stack_entry = {{0}};
+
+void S_SetStackAdjustmentStart(void)
+{
+	if (!pause_starttic)
+		pause_starttic = gametic;
+}
+
+void S_AdjustMusicStackTics(void)
+{
+	if (pause_starttic)
+	{
+		size_t i;
+		for (i = 0; i < NUMMUSICSTACKS-1; i++)
+		{
+			if (!music_stack[i].status)
+				break;
+			music_stack[i].tic += gametic - pause_starttic;
+		}
+		pause_starttic = 0;
+	}
+}
+
+static void S_ResetMusicStack()
+{
+	size_t i;
+	for (i = 0; i < NUMMUSICSTACKS; i++)
+		music_stack[i] = empty_music_stack_entry;
+}
+
+static void S_RemoveMusicStackEntry(size_t i)
+{
+	for (; i < NUMMUSICSTACKS-1; i++)
+	{
+		strncpy(music_stack[i].musname, music_stack[i+1].musname, 7);
+		music_stack[i].musname[6] = 0;
+		music_stack[i].musflags = music_stack[i+1].musflags;
+		music_stack[i].looping = music_stack[i+1].looping;
+		music_stack[i].position = music_stack[i+1].position;
+		music_stack[i].tic = music_stack[i+1].tic;
+		music_stack[i].status = music_stack[i+1].status;
+
+		if (!music_stack[i].status)
+			break;
+	}
+
+	// clear the last slot
+	music_stack[NUMMUSICSTACKS-1] = empty_music_stack_entry;
+}
+
+static void S_RemoveMusicStackEntryByStatus(UINT16 status)
+{
+	int i;
+
+	if (!status)
+		return;
+
+	for (i = 0; i < NUMMUSICSTACKS-1; i++)
+	{
+		if (music_stack[i].status == status)
+		{
+			S_RemoveMusicStackEntry(i);
+			i--; // try this position again
+		}
+		else if (!music_stack[i].status)
+			break;
+	}
+}
+
+static void S_AddMusicStackEntry(const char *mname, UINT16 mflags, boolean looping, UINT32 position, UINT16 status)
+{
+	size_t i;
+
+	// if the first entry is empty, force master onto it
+	if (!music_stack[0].status && status != JT_MASTER)
+		S_AddMusicStackEntry(mapmusname, mapmusflags, true, S_GetMusicPosition(), JT_MASTER);
+
+	// are all slots taken? forget the earliest one (save master) and move down the rest
+	if (music_stack[NUMMUSICSTACKS-1].status)
+		S_RemoveMusicStackEntry(1);
+
+	// look for an empty slot to park ourselves
+	for (i = 0; i < NUMMUSICSTACKS; i++)
+	{
+		// entry doesn't exist? park ourselves here!
+		if (!music_stack[i].status)
+		{
+			strncpy(music_stack[i].musname, mname, 7);
+			music_stack[i].musname[6] = 0;
+			music_stack[i].musflags = mflags;
+			music_stack[i].looping = looping;
+			music_stack[i].position = position;
+			music_stack[i].tic = gametic;
+			music_stack[i].status = status;
+			break;
+		}
+	}
+}
+
+static musicstack_t S_GetMusicStackEntry(UINT16 status, boolean fromfirst, INT16 startindex)
+{
+	size_t i;
+
+	// if the first entry is empty, force master onto it
+	// fixes a memory corruption bug
+	if (!music_stack[0].status && status != JT_MASTER)
+		S_AddMusicStackEntry(mapmusname, mapmusflags, true, S_GetMusicPosition(), JT_MASTER);
+
+	if (startindex < 0)
+		startindex = fromfirst ? 0 : NUMMUSICSTACKS-1;
+
+	if (fromfirst)
+	{
+		for (i = startindex; i < NUMMUSICSTACKS; i++)
+		{
+			if (!music_stack[i].status) // we're counting up, so this must mean we reached the end
+				break;
+			else if (!status || music_stack[i].status == status)
+			{
+				if (P_EvaluateMusicStatus(music_stack[i].status))
+				{
+					if (!S_MusicExists(music_stack[i].musname, !midi_disabled, !digital_disabled)) // paranoia
+						S_RemoveMusicStackEntry(i); // then continue
+					else
+						return music_stack[i];
+				}
+				else
+					S_RemoveMusicStackEntry(i); // then continue
+			}
+		}
+	}
+	else
+	{
+		for (i = startindex; i >= 0; i--)
+		{
+			if (!music_stack[i].status) // since we're counting down, we have to skip a few...
+				continue;
+			else if (!status || music_stack[i].status == status)
+			{
+				if (P_EvaluateMusicStatus(music_stack[i].status))
+				{
+					if (!S_MusicExists(music_stack[i].musname, !midi_disabled, !digital_disabled)) // paranoia
+						S_RemoveMusicStackEntry(i); // then continue
+					else
+						return music_stack[i];
+				}
+				else
+					S_RemoveMusicStackEntry(i); // then continue
+			}
+		}
+	}
+
+	return empty_music_stack_entry;
+}
+
+void S_RetainMusic(const char *mname, UINT16 mflags, boolean looping, UINT32 position, UINT16 status)
+{
+	size_t i;
+
+	if (!status) // we use this as a null indicator, don't push
+	{
+		CONS_Alert(CONS_ERROR, "Music stack entry must have a nonzero status.\n");
+		return;
+	}
+	else if (status == JT_MASTER) // enforce only one JT_MASTER
+	{
+		for (i = 0; i < NUMMUSICSTACKS; i++)
+		{
+			if (music_stack[i].status == JT_MASTER)
+			{
+				CONS_Alert(CONS_ERROR, "Music stack can only have one JT_MASTER entry.\n");
+				return;
+			}
+		}
+	}
+	else // remove any existing status
+		S_RemoveMusicStackEntryByStatus(status);
+
+	S_AddMusicStackEntry(mname, mflags, looping, position, status);
+}
+
+boolean S_RecallMusic(UINT16 status, boolean fromfirst)
+{
+	UINT32 newpos = 0;
+	boolean mapmuschanged = false;
+	musicstack_t entry;
+
+	if (status)
+		entry = S_GetMusicStackEntry(status, fromfirst, -1);
+	else
+		entry = S_GetMusicStackEntry(JT_NONE, false, -1);
+
+	if (!S_MusicExists(entry.musname, !midi_disabled, !digital_disabled))
+		return false; // bad bad bad!!
+
+	// no result, just grab mapmusname
+	if (!entry.musname[0] || ((status == JT_MASTER || !music_stack[0].status) && !entry.status))
+	{
+		strncpy(entry.musname, mapmusname, 7);
+		entry.musflags = mapmusflags;
+		entry.looping = true;
+		entry.position = mapmusposition;
+		entry.tic = gametic;
+		entry.status = JT_MASTER;
+	}
+
+	if (entry.status == JT_MASTER)
+	{
+		mapmuschanged = (boolean)strnicmp(entry.musname, mapmusname, 7);
+		S_ResetMusicStack();
+	}
+	else
+	{
+		if (!entry.status)
+			return false;
+	}
+
+	if (!mapmuschanged && strncmp(entry.musname, S_MusicName(), 7)) // don't restart music if we're already playing it
+	{
+		if (music_stack_fadeout)
+			S_ChangeMusicEx(entry.musname, entry.musflags, entry.looping, 0, music_stack_fadeout, 0);
+		else
+		{
+			S_ChangeMusicEx(entry.musname, entry.musflags, entry.looping, 0, 0, music_stack_fadein);
+			if (!music_stack_noposition) // HACK: Global boolean to toggle position resuming, e.g., de-superize
+				newpos = entry.position + (S_GetMusicLength() ? (UINT32)((float)(gametic - entry.tic)/(float)TICRATE*(float)MUSICRATE) : 0);
+
+			if (newpos > 0 && S_MusicPlaying())
+				S_SetMusicPosition(newpos);
+			else
+			{
+				S_StopFadingMusic();
+				S_SetInternalMusicVolume(100);
+			}
+		}
+		music_stack_noposition = false;
+		music_stack_fadeout = 0;
+		music_stack_fadein = JINGLEPOSTFADE;
+	}
+
+	return true;
+}
+
 /// ------------------------
 /// Music Playback
 /// ------------------------
@@ -1529,6 +1781,8 @@ void S_PauseAudio(void)
 #else
 	I_StopCD();
 #endif
+
+	S_SetStackAdjustmentStart();
 }
 
 void S_ResumeAudio(void)
@@ -1538,6 +1792,8 @@ void S_ResumeAudio(void)
 
 	// resume cd music
 	I_ResumeCD();
+
+	S_AdjustMusicStackTics();
 }
 
 void S_SetMusicVolume(INT32 digvolume, INT32 seqvolume)
@@ -1623,4 +1879,9 @@ void S_Start(void)
 	if (cv_resetmusic.value)
 		S_StopMusic();
 	S_ChangeMusicEx(mapmusname, mapmusflags, true, mapmusposition, 0, 0);
+
+	S_ResetMusicStack();
+	music_stack_noposition = false;
+	music_stack_fadeout = 0;
+	music_stack_fadein = JINGLEPOSTFADE;
 }
diff --git a/src/s_sound.h b/src/s_sound.h
index 538707ffb..241634b12 100644
--- a/src/s_sound.h
+++ b/src/s_sound.h
@@ -140,6 +140,30 @@ boolean S_SetMusicPosition(UINT32 position);
 // Get Position of Music
 UINT32 S_GetMusicPosition(void);
 
+//
+// Music Stacking (Jingles)
+//
+
+typedef struct {
+	char musname[7];
+	UINT16 musflags;
+	boolean looping;
+	UINT32 position;
+	tic_t tic;
+	UINT16 status;
+} musicstack_t;
+
+char music_stack_nextmusname[7];
+boolean music_stack_noposition;
+UINT32 music_stack_fadeout;
+UINT32 music_stack_fadein;
+#define NUMMUSICSTACKS 10 // hahaha wait until someone needs > 10 resumes
+
+void S_SetStackAdjustmentStart(void);
+void S_AdjustMusicStackTics(void);
+void S_RetainMusic(const char *mname, UINT16 mflags, boolean looping, UINT32 position, UINT16 status);
+boolean S_RecallMusic(UINT16 status, boolean fromfirst);
+
 //
 // Music Playback
 //
diff --git a/src/sdl/i_video.c b/src/sdl/i_video.c
index 30ef1b27b..2a13c57c8 100644
--- a/src/sdl/i_video.c
+++ b/src/sdl/i_video.c
@@ -62,7 +62,6 @@
 #include "../m_menu.h"
 #include "../d_main.h"
 #include "../s_sound.h"
-#include "../i_sound.h"  // midi pause/unpause
 #include "../i_joy.h"
 #include "../st_stuff.h"
 #include "../g_game.h"
@@ -566,7 +565,7 @@ static void Impl_HandleWindowEvent(SDL_WindowEvent evt)
 		// Tell game we got focus back, resume music if necessary
 		window_notinfocus = false;
 		if (!paused)
-			I_ResumeSong(); //resume it
+			S_ResumeAudio(); //resume it
 
 		if (!firsttimeonmouse)
 		{
@@ -578,7 +577,7 @@ static void Impl_HandleWindowEvent(SDL_WindowEvent evt)
 	{
 		// Tell game we lost focus, pause music
 		window_notinfocus = true;
-		I_PauseSong();
+		S_PauseAudio();
 
 		if (!disable_mouse)
 		{
diff --git a/src/win32/win_main.c b/src/win32/win_main.c
index bfe620a43..8a29f7e18 100644
--- a/src/win32/win_main.c
+++ b/src/win32/win_main.c
@@ -42,7 +42,7 @@
 #include "fabdxlib.h"
 #include "win_main.h"
 #include "win_dbg.h"
-#include "../i_sound.h" // midi pause/unpause
+#include "../s_sound.h" // pause sound with handling
 #include "../g_input.h" // KEY_MOUSEWHEELxxx
 #include "../screen.h" // for BASEVID*
 
@@ -110,9 +110,9 @@ static LRESULT CALLBACK MainWndproc(HWND hWnd, UINT message, WPARAM wParam, LPAR
 
 			// pause music when alt-tab
 			if (appActive  && !paused)
-				I_ResumeSong();
+				S_ResumeAudio();
 			else if (!paused)
-				I_PauseSong();
+				S_PauseAudio();
 			{
 				HANDLE ci = GetStdHandle(STD_INPUT_HANDLE);
 				DWORD mode;

From be2342799ed673cd767b15af5df7acc49a312cd5 Mon Sep 17 00:00:00 2001
From: mazmazz <mar.marcoz@outlook.com>
Date: Tue, 18 Sep 2018 15:10:42 -0400
Subject: [PATCH 02/10] Lua P_RestoreMusic else if fix

---
 src/lua_baselib.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/lua_baselib.c b/src/lua_baselib.c
index 6a8784259..093683660 100644
--- a/src/lua_baselib.c
+++ b/src/lua_baselib.c
@@ -719,7 +719,7 @@ static int lib_pRestoreMusic(lua_State *L)
 	NOHUD
 	if (!player)
 		return LUA_ErrInvalid(L, "player_t");
-	else if (P_IsLocalPlayer(player))
+	if (P_IsLocalPlayer(player))
 		P_RestoreMusic(player);
 	return 0;
 }

From e08bdec216b6c8cfa06a0a011fd07785e6aff321 Mon Sep 17 00:00:00 2001
From: mazmazz <mar.marcoz@outlook.com>
Date: Wed, 19 Sep 2018 18:42:11 -0400
Subject: [PATCH 03/10] MP Jingle: Buildbot fixes

---
 src/s_sound.c | 14 ++++++++++++--
 1 file changed, 12 insertions(+), 2 deletions(-)

diff --git a/src/s_sound.c b/src/s_sound.c
index bec6b1ba2..345ba0449 100644
--- a/src/s_sound.c
+++ b/src/s_sound.c
@@ -1336,8 +1336,18 @@ UINT32 S_GetMusicPosition(void)
 /// In this section: mazmazz doesn't know how to do dynamic arrays or struct pointers!
 /// ------------------------
 
+// make the buildbots happy, because this safely zeroes out a struct
+// https://stackoverflow.com/q/1538943
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wmissing-field-initializers"
+
 static const musicstack_t empty_music_stack_entry = {{0}};
 
+#pragma clang diagnostic pop
+#pragma GCC diagnostic pop
+
 void S_SetStackAdjustmentStart(void)
 {
 	if (!pause_starttic)
@@ -1437,7 +1447,7 @@ static void S_AddMusicStackEntry(const char *mname, UINT16 mflags, boolean loopi
 
 static musicstack_t S_GetMusicStackEntry(UINT16 status, boolean fromfirst, INT16 startindex)
 {
-	size_t i;
+	INT16 i;
 
 	// if the first entry is empty, force master onto it
 	// fixes a memory corruption bug
@@ -1469,7 +1479,7 @@ static musicstack_t S_GetMusicStackEntry(UINT16 status, boolean fromfirst, INT16
 	}
 	else
 	{
-		for (i = startindex; i >= 0; i--)
+		for (i = startindex; i >= 0; i--) // this line is why i is signed; otherwise, would wrap around to max value
 		{
 			if (!music_stack[i].status) // since we're counting down, we have to skip a few...
 				continue;

From 17c796fe74fb591e6c982ed39b31419faa5b5c27 Mon Sep 17 00:00:00 2001
From: mazmazz <mar.marcoz@outlook.com>
Date: Wed, 19 Sep 2018 19:24:35 -0400
Subject: [PATCH 04/10] MP Jingle: Buildbot fixes

---
 src/s_sound.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/s_sound.c b/src/s_sound.c
index 345ba0449..9a5e7247b 100644
--- a/src/s_sound.c
+++ b/src/s_sound.c
@@ -1369,7 +1369,7 @@ void S_AdjustMusicStackTics(void)
 	}
 }
 
-static void S_ResetMusicStack()
+static void S_ResetMusicStack(void)
 {
 	size_t i;
 	for (i = 0; i < NUMMUSICSTACKS; i++)
@@ -1554,7 +1554,7 @@ boolean S_RecallMusic(UINT16 status, boolean fromfirst)
 
 	if (entry.status == JT_MASTER)
 	{
-		mapmuschanged = (boolean)strnicmp(entry.musname, mapmusname, 7);
+		mapmuschanged = strnicmp(entry.musname, mapmusname, 7);
 		S_ResetMusicStack();
 	}
 	else

From af5aecb2460e325bcea316de02669dfd59f1ce4b Mon Sep 17 00:00:00 2001
From: mazmazz <mar.marcoz@outlook.com>
Date: Wed, 19 Sep 2018 19:51:04 -0400
Subject: [PATCH 05/10] MP Jingle: Better empty_music_stack_entry
 initialization (buildbots)

---
 src/s_sound.c | 12 +-----------
 1 file changed, 1 insertion(+), 11 deletions(-)

diff --git a/src/s_sound.c b/src/s_sound.c
index 3708673e3..2f53776bd 100644
--- a/src/s_sound.c
+++ b/src/s_sound.c
@@ -1336,17 +1336,7 @@ UINT32 S_GetMusicPosition(void)
 /// In this section: mazmazz doesn't know how to do dynamic arrays or struct pointers!
 /// ------------------------
 
-// make the buildbots happy, because this safely zeroes out a struct
-// https://stackoverflow.com/q/1538943
-#pragma GCC diagnostic push
-#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
-#pragma clang diagnostic push
-#pragma clang diagnostic ignored "-Wmissing-field-initializers"
-
-static const musicstack_t empty_music_stack_entry = {{0}};
-
-#pragma clang diagnostic pop
-#pragma GCC diagnostic pop
+static const musicstack_t empty_music_stack_entry = {"\0", 0, false, 0, 0, 0};
 
 void S_SetStackAdjustmentStart(void)
 {

From 78c1b99cd4434d6257ec92e3c248eda36830ecf9 Mon Sep 17 00:00:00 2001
From: mazmazz <mar.marcoz@outlook.com>
Date: Thu, 20 Sep 2018 16:37:14 -0400
Subject: [PATCH 06/10] MP Jingle: Changed music_stacks from fixed array to
 next/prev chain

---
 src/s_sound.c | 238 +++++++++++++++++++++++++-------------------------
 src/s_sound.h |   7 +-
 2 files changed, 126 insertions(+), 119 deletions(-)

diff --git a/src/s_sound.c b/src/s_sound.c
index f8e0376b4..3fa060340 100644
--- a/src/s_sound.c
+++ b/src/s_sound.c
@@ -1229,8 +1229,6 @@ static boolean   queue_looping;
 static UINT32    queue_position;
 static UINT32    queue_fadeinms;
 
-static musicstack_t music_stack[NUMMUSICSTACKS];
-
 static tic_t     pause_starttic;
 
 /// ------------------------
@@ -1336,7 +1334,8 @@ UINT32 S_GetMusicPosition(void)
 /// In this section: mazmazz doesn't know how to do dynamic arrays or struct pointers!
 /// ------------------------
 
-static const musicstack_t empty_music_stack_entry = {"\0", 0, false, 0, 0, 0};
+static musicstack_t *music_stacks = NULL;
+static musicstack_t *last_music_stack = NULL;
 
 void S_SetStackAdjustmentStart(void)
 {
@@ -1348,152 +1347,145 @@ void S_AdjustMusicStackTics(void)
 {
 	if (pause_starttic)
 	{
-		size_t i;
-		for (i = 0; i < NUMMUSICSTACKS-1; i++)
-		{
-			if (!music_stack[i].status)
-				break;
-			music_stack[i].tic += gametic - pause_starttic;
-		}
+		musicstack_t *mst;
+		for (mst = music_stacks; mst; mst = mst->next)
+			mst->tic += gametic - pause_starttic;
 		pause_starttic = 0;
 	}
 }
 
 static void S_ResetMusicStack(void)
 {
-	size_t i;
-	for (i = 0; i < NUMMUSICSTACKS; i++)
-		music_stack[i] = empty_music_stack_entry;
+	musicstack_t *mst, *mst_next;
+	for (mst = music_stacks; mst; mst = mst_next)
+	{
+		mst_next = mst->next;
+		Z_Free(mst);
+	}
+	music_stacks = last_music_stack = NULL;
 }
 
-static void S_RemoveMusicStackEntry(size_t i)
+static void S_RemoveMusicStackEntry(musicstack_t *entry)
 {
-	for (; i < NUMMUSICSTACKS-1; i++)
+	musicstack_t *mst;
+	for (mst = music_stacks; mst; mst = mst->next)
 	{
-		strncpy(music_stack[i].musname, music_stack[i+1].musname, 7);
-		music_stack[i].musname[6] = 0;
-		music_stack[i].musflags = music_stack[i+1].musflags;
-		music_stack[i].looping = music_stack[i+1].looping;
-		music_stack[i].position = music_stack[i+1].position;
-		music_stack[i].tic = music_stack[i+1].tic;
-		music_stack[i].status = music_stack[i+1].status;
+		if (mst == entry)
+		{
+			// Remove ourselves from the chain and link
+			// prev and next together
+
+			if (mst->prev)
+				mst->prev->next = mst->next;
+			else
+				music_stacks = mst->next;
+
+			if (mst->next)
+				mst->next->prev = mst->prev;
+			else
+				last_music_stack = mst->prev;
 
-		if (!music_stack[i].status)
 			break;
+		}
 	}
-
-	// clear the last slot
-	music_stack[NUMMUSICSTACKS-1] = empty_music_stack_entry;
+	Z_Free(entry);
 }
 
 static void S_RemoveMusicStackEntryByStatus(UINT16 status)
 {
-	int i;
+	musicstack_t *mst, *mst_next;
 
 	if (!status)
 		return;
 
-	for (i = 0; i < NUMMUSICSTACKS-1; i++)
+	for (mst = music_stacks; mst; mst = mst_next)
 	{
-		if (music_stack[i].status == status)
-		{
-			S_RemoveMusicStackEntry(i);
-			i--; // try this position again
-		}
-		else if (!music_stack[i].status)
-			break;
+		mst_next = mst->next;
+		if (mst->status == status)
+			S_RemoveMusicStackEntry(mst);
 	}
 }
 
 static void S_AddMusicStackEntry(const char *mname, UINT16 mflags, boolean looping, UINT32 position, UINT16 status)
 {
-	size_t i;
+	musicstack_t *mst, *new_mst;
 
 	// if the first entry is empty, force master onto it
-	if (!music_stack[0].status && status != JT_MASTER)
-		S_AddMusicStackEntry(mapmusname, mapmusflags, true, S_GetMusicPosition(), JT_MASTER);
+	if (!music_stacks)
+	{
+		music_stacks = Z_Calloc(sizeof (*mst), PU_MUSIC, NULL);
+		strncpy(music_stacks->musname, (status == JT_MASTER ? mname : mapmusname), 7);
+		music_stacks->musflags = (status == JT_MASTER ? mflags : mapmusflags);
+		music_stacks->looping = (status == JT_MASTER ? looping : true);
+		music_stacks->position = (status == JT_MASTER ? position : S_GetMusicPosition());
+		music_stacks->tic = gametic;
+		music_stacks->status = JT_MASTER;
 
-	// are all slots taken? forget the earliest one (save master) and move down the rest
-	if (music_stack[NUMMUSICSTACKS-1].status)
-		S_RemoveMusicStackEntry(1);
+		if (status == JT_MASTER)
+			return; // we just added the user's entry here
+	}
 
 	// look for an empty slot to park ourselves
-	for (i = 0; i < NUMMUSICSTACKS; i++)
-	{
-		// entry doesn't exist? park ourselves here!
-		if (!music_stack[i].status)
-		{
-			strncpy(music_stack[i].musname, mname, 7);
-			music_stack[i].musname[6] = 0;
-			music_stack[i].musflags = mflags;
-			music_stack[i].looping = looping;
-			music_stack[i].position = position;
-			music_stack[i].tic = gametic;
-			music_stack[i].status = status;
-			break;
-		}
-	}
+	for (mst = music_stacks; mst->next; mst = mst->next);
+
+	// create our new entry
+	new_mst = Z_Calloc(sizeof (*new_mst), PU_MUSIC, NULL);
+	strncpy(new_mst->musname, mname, 7);
+	new_mst->musname[6] = 0;
+	new_mst->musflags = mflags;
+	new_mst->looping = looping;
+	new_mst->position = position;
+	new_mst->tic = gametic;
+	new_mst->status = status;
+
+	mst->next = new_mst;
+	new_mst->prev = mst;
+	new_mst->next = NULL;
+	last_music_stack = new_mst;
 }
 
-static musicstack_t S_GetMusicStackEntry(UINT16 status, boolean fromfirst, INT16 startindex)
+static musicstack_t *S_GetMusicStackEntry(UINT16 status, boolean fromfirst, INT16 startindex)
 {
-	INT16 i;
+	musicstack_t *mst, *start_mst = NULL, *mst_next;
 
 	// if the first entry is empty, force master onto it
 	// fixes a memory corruption bug
-	if (!music_stack[0].status && status != JT_MASTER)
+	if (!music_stacks && status != JT_MASTER)
 		S_AddMusicStackEntry(mapmusname, mapmusflags, true, S_GetMusicPosition(), JT_MASTER);
 
-	if (startindex < 0)
-		startindex = fromfirst ? 0 : NUMMUSICSTACKS-1;
-
-	if (fromfirst)
+	if (startindex >= 0)
 	{
-		for (i = startindex; i < NUMMUSICSTACKS; i++)
-		{
-			if (!music_stack[i].status) // we're counting up, so this must mean we reached the end
-				break;
-			else if (!status || music_stack[i].status == status)
-			{
-				if (P_EvaluateMusicStatus(music_stack[i].status))
-				{
-					if (!S_MusicExists(music_stack[i].musname, !midi_disabled, !digital_disabled)) // paranoia
-						S_RemoveMusicStackEntry(i); // then continue
-					else
-						return music_stack[i];
-				}
-				else
-					S_RemoveMusicStackEntry(i); // then continue
-			}
-		}
+		INT16 i = 0;
+		for (mst = music_stacks; mst && i <= startindex; mst = mst->next, i++)
+			start_mst = mst;
 	}
 	else
+		start_mst = (fromfirst ? music_stacks : last_music_stack);
+
+	for (mst = start_mst; mst; mst = mst_next)
 	{
-		for (i = startindex; i >= 0; i--) // this line is why i is signed; otherwise, would wrap around to max value
+		mst_next = (fromfirst ? mst->next : mst->prev);
+
+		if (!status || mst->status == status)
 		{
-			if (!music_stack[i].status) // since we're counting down, we have to skip a few...
-				continue;
-			else if (!status || music_stack[i].status == status)
+			if (P_EvaluateMusicStatus(mst->status))
 			{
-				if (P_EvaluateMusicStatus(music_stack[i].status))
-				{
-					if (!S_MusicExists(music_stack[i].musname, !midi_disabled, !digital_disabled)) // paranoia
-						S_RemoveMusicStackEntry(i); // then continue
-					else
-						return music_stack[i];
-				}
+				if (!S_MusicExists(mst->musname, !midi_disabled, !digital_disabled)) // paranoia
+					S_RemoveMusicStackEntry(mst); // then continue
 				else
-					S_RemoveMusicStackEntry(i); // then continue
+					return mst;
 			}
+			else
+				S_RemoveMusicStackEntry(mst); // then continue
 		}
 	}
 
-	return empty_music_stack_entry;
+	return NULL;
 }
 
 void S_RetainMusic(const char *mname, UINT16 mflags, boolean looping, UINT32 position, UINT16 status)
 {
-	size_t i;
+	musicstack_t *mst;
 
 	if (!status) // we use this as a null indicator, don't push
 	{
@@ -1502,9 +1494,9 @@ void S_RetainMusic(const char *mname, UINT16 mflags, boolean looping, UINT32 pos
 	}
 	else if (status == JT_MASTER) // enforce only one JT_MASTER
 	{
-		for (i = 0; i < NUMMUSICSTACKS; i++)
+		for (mst = music_stacks; mst; mst = mst->next)
 		{
-			if (music_stack[i].status == JT_MASTER)
+			if (mst->status == JT_MASTER)
 			{
 				CONS_Alert(CONS_ERROR, "Music stack can only have one JT_MASTER entry.\n");
 				return;
@@ -1521,47 +1513,58 @@ boolean S_RecallMusic(UINT16 status, boolean fromfirst)
 {
 	UINT32 newpos = 0;
 	boolean mapmuschanged = false;
-	musicstack_t entry;
+	musicstack_t *result;
+	musicstack_t *entry = Z_Calloc(sizeof (*result), PU_MUSIC, NULL);
 
 	if (status)
-		entry = S_GetMusicStackEntry(status, fromfirst, -1);
+		result = S_GetMusicStackEntry(status, fromfirst, -1);
 	else
-		entry = S_GetMusicStackEntry(JT_NONE, false, -1);
+		result = S_GetMusicStackEntry(JT_NONE, false, -1);
 
-	if (!S_MusicExists(entry.musname, !midi_disabled, !digital_disabled))
-		return false; // bad bad bad!!
+	if (result && !S_MusicExists(result->musname, !midi_disabled, !digital_disabled))
+	{
+		Z_Free(entry);
+		return false; // music doesn't exist, so don't do anything
+	}
+
+	// make a copy of result, since we make modifications to our copy
+	if (result)
+	{
+		*entry = *result;
+		strncpy(entry->musname, result->musname, 7);
+	}
 
 	// no result, just grab mapmusname
-	if (!entry.musname[0] || ((status == JT_MASTER || !music_stack[0].status) && !entry.status))
+	if (!result || !entry->musname[0] || ((status == JT_MASTER || (music_stacks ? !music_stacks->status : false)) && !entry->status))
 	{
-		strncpy(entry.musname, mapmusname, 7);
-		entry.musflags = mapmusflags;
-		entry.looping = true;
-		entry.position = mapmusposition;
-		entry.tic = gametic;
-		entry.status = JT_MASTER;
+		strncpy(entry->musname, mapmusname, 7);
+		entry->musflags = mapmusflags;
+		entry->looping = true;
+		entry->position = mapmusposition;
+		entry->tic = gametic;
+		entry->status = JT_MASTER;
 	}
 
-	if (entry.status == JT_MASTER)
+	if (entry->status == JT_MASTER)
 	{
-		mapmuschanged = strnicmp(entry.musname, mapmusname, 7);
+		mapmuschanged = strnicmp(entry->musname, mapmusname, 7);
 		S_ResetMusicStack();
 	}
-	else
+	else if (!entry->status)
 	{
-		if (!entry.status)
-			return false;
+		Z_Free(entry);
+		return false;
 	}
 
-	if (!mapmuschanged && strncmp(entry.musname, S_MusicName(), 7)) // don't restart music if we're already playing it
+	if (!mapmuschanged && strncmp(entry->musname, S_MusicName(), 7)) // don't restart music if we're already playing it
 	{
 		if (music_stack_fadeout)
-			S_ChangeMusicEx(entry.musname, entry.musflags, entry.looping, 0, music_stack_fadeout, 0);
+			S_ChangeMusicEx(entry->musname, entry->musflags, entry->looping, 0, music_stack_fadeout, 0);
 		else
 		{
-			S_ChangeMusicEx(entry.musname, entry.musflags, entry.looping, 0, 0, music_stack_fadein);
+			S_ChangeMusicEx(entry->musname, entry->musflags, entry->looping, 0, 0, music_stack_fadein);
 			if (!music_stack_noposition) // HACK: Global boolean to toggle position resuming, e.g., de-superize
-				newpos = entry.position + (S_GetMusicLength() ? (UINT32)((float)(gametic - entry.tic)/(float)TICRATE*(float)MUSICRATE) : 0);
+				newpos = entry->position + (S_GetMusicLength() ? (UINT32)((float)(gametic - entry->tic)/(float)TICRATE*(float)MUSICRATE) : 0);
 
 			if (newpos > 0 && S_MusicPlaying())
 				S_SetMusicPosition(newpos);
@@ -1576,6 +1579,7 @@ boolean S_RecallMusic(UINT16 status, boolean fromfirst)
 		music_stack_fadein = JINGLEPOSTFADE;
 	}
 
+	Z_Free(entry);
 	return true;
 }
 
diff --git a/src/s_sound.h b/src/s_sound.h
index 241634b12..dcf851a87 100644
--- a/src/s_sound.h
+++ b/src/s_sound.h
@@ -144,20 +144,23 @@ UINT32 S_GetMusicPosition(void);
 // Music Stacking (Jingles)
 //
 
-typedef struct {
+typedef struct musicstack_s
+{
 	char musname[7];
 	UINT16 musflags;
 	boolean looping;
 	UINT32 position;
 	tic_t tic;
 	UINT16 status;
+
+    struct musicstack_s *prev;
+    struct musicstack_s *next;
 } musicstack_t;
 
 char music_stack_nextmusname[7];
 boolean music_stack_noposition;
 UINT32 music_stack_fadeout;
 UINT32 music_stack_fadein;
-#define NUMMUSICSTACKS 10 // hahaha wait until someone needs > 10 resumes
 
 void S_SetStackAdjustmentStart(void);
 void S_AdjustMusicStackTics(void);

From d8addda298e97f1c566b3da1680379cd59207188 Mon Sep 17 00:00:00 2001
From: mazmazz <mar.marcoz@outlook.com>
Date: Thu, 20 Sep 2018 18:30:21 -0400
Subject: [PATCH 07/10] MP Jingle: Add fade timing hack after ending a
 non-looping song

---
 src/sdl/mixer_sound.c | 25 +++++++++++++++++++++++--
 1 file changed, 23 insertions(+), 2 deletions(-)

diff --git a/src/sdl/mixer_sound.c b/src/sdl/mixer_sound.c
index fc6ccf50c..85c4c23c1 100644
--- a/src/sdl/mixer_sound.c
+++ b/src/sdl/mixer_sound.c
@@ -84,6 +84,7 @@ static UINT32 fading_timer;
 static UINT32 fading_duration;
 static INT32 fading_id;
 static void (*fading_callback)(void);
+static boolean fading_nocleanup;
 
 #ifdef HAVE_LIBGME
 static Music_Emu *gme;
@@ -99,7 +100,12 @@ static void var_cleanup(void)
 	songpaused = is_looping =\
 	 is_fading = false;
 
-	fading_callback = NULL;
+	// HACK: See music_loop, where we want the fade timing to proceed after a non-looping
+	// song has stopped playing
+	if (!fading_nocleanup)
+		fading_callback = NULL;
+	else
+		fading_nocleanup = false; // use it once, set it back immediately
 
 	internal_volume = 100;
 }
@@ -125,6 +131,8 @@ void I_StartupSound(void)
 		return;
 	}
 
+	fading_nocleanup = false;
+
 	var_cleanup();
 
 	music = NULL;
@@ -537,7 +545,15 @@ static void music_loop(void)
 		music_bytes = loop_point*44100.0L*4; //assume 44.1khz, 4-byte length (see I_GetSongPosition)
 	}
 	else
+	{
+		// HACK: Let fade timing proceed beyond the end of a
+		// non-looping song. This is a specific case where the timing
+		// should persist after stopping a song, so I don't believe
+		// this should apply every time the user stops a song.
+		// This is auto-unset in var_cleanup, called by I_StopSong
+		fading_nocleanup = true;
 		I_StopSong();
+	}
 }
 
 static UINT32 music_fade(UINT32 interval, void *param)
@@ -1151,7 +1167,10 @@ boolean I_PlaySong(boolean looping)
 
 void I_StopSong(void)
 {
-	I_StopFadingSong();
+	// HACK: See music_loop on why we want fade timing to proceed
+	// after end of song
+	if (!fading_nocleanup)
+		I_StopFadingSong();
 
 #ifdef HAVE_LIBGME
 	if (gme)
@@ -1269,6 +1288,8 @@ void I_StopFadingSong(void)
 		SDL_RemoveTimer(fading_id);
 	is_fading = false;
 	fading_source = fading_target = fading_timer = fading_duration = fading_id = 0;
+	// don't unset fading_nocleanup here just yet; fading_callback is cleaned up
+	// in var_cleanup()
 }
 
 boolean I_FadeSongFromVolume(UINT8 target_volume, UINT8 source_volume, UINT32 ms, void (*callback)(void))

From d14db911331c972db611aa68899f1399cd40192f Mon Sep 17 00:00:00 2001
From: mazmazz <mar.marcoz@outlook.com>
Date: Thu, 20 Sep 2018 18:57:37 -0400
Subject: [PATCH 08/10] MP Jingle: Remove P_RestoreMusic from space countdown
 (no reason to have it)

---
 src/p_enemy.c | 4 +---
 src/p_user.c  | 3 ---
 2 files changed, 1 insertion(+), 6 deletions(-)

diff --git a/src/p_enemy.c b/src/p_enemy.c
index 11e3856c4..ceeb6c97d 100644
--- a/src/p_enemy.c
+++ b/src/p_enemy.c
@@ -3232,10 +3232,8 @@ void A_WaterShield(mobj_t *actor)
 	player->powers[pw_underwater] = 0;
 
 	if (player->powers[pw_spacetime] > 1)
-	{
 		player->powers[pw_spacetime] = 0;
-		P_RestoreMusic(player);
-	}
+
 	S_StartSound(player->mo, actor->info->seesound);
 }
 
diff --git a/src/p_user.c b/src/p_user.c
index cb14814a2..ea58b7eeb 100644
--- a/src/p_user.c
+++ b/src/p_user.c
@@ -2255,10 +2255,7 @@ static void P_CheckUnderwaterAndSpaceTimer(player_t *player)
 	}
 
 	if (player->powers[pw_spacetime] > 1 && !P_InSpaceSector(player->mo))
-	{
 		player->powers[pw_spacetime] = 0;
-		P_RestoreMusic(player);
-	}
 
 	// Underwater audio cues
 	if (P_IsLocalPlayer(player) && !player->bot)

From e0f9cdcfd1a78edea2fbb5c81adc1a9d3587f572 Mon Sep 17 00:00:00 2001
From: mazmazz <mar.marcoz@outlook.com>
Date: Fri, 21 Sep 2018 15:42:26 -0400
Subject: [PATCH 09/10] MP Jingle: Fix underwater music not restoring on air
 bubble or shield

---
 src/p_enemy.c | 7 +++++--
 src/p_inter.c | 2 ++
 src/p_user.c  | 7 +++++--
 3 files changed, 12 insertions(+), 4 deletions(-)

diff --git a/src/p_enemy.c b/src/p_enemy.c
index ceeb6c97d..1f57d1901 100644
--- a/src/p_enemy.c
+++ b/src/p_enemy.c
@@ -3227,9 +3227,12 @@ void A_WaterShield(mobj_t *actor)
 	}
 
 	if (player->powers[pw_underwater] && player->powers[pw_underwater] <= 12*TICRATE + 1)
+	{
+		player->powers[pw_underwater] = 0;
 		P_RestoreMusic(player);
-
-	player->powers[pw_underwater] = 0;
+	}
+	else
+		player->powers[pw_underwater] = 0;
 
 	if (player->powers[pw_spacetime] > 1)
 		player->powers[pw_spacetime] = 0;
diff --git a/src/p_inter.c b/src/p_inter.c
index 7a17fbcf6..2c5c85ed3 100644
--- a/src/p_inter.c
+++ b/src/p_inter.c
@@ -1429,7 +1429,9 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 
 			// Eaten by player!
 			if (player->powers[pw_underwater] && player->powers[pw_underwater] <= 12*TICRATE + 1)
+				player->powers[pw_underwater] = underwatertics + 1;
 				P_RestoreMusic(player);
+			}
 
 			if (player->powers[pw_underwater] < underwatertics + 1)
 				player->powers[pw_underwater] = underwatertics + 1;
diff --git a/src/p_user.c b/src/p_user.c
index ea58b7eeb..f41459147 100644
--- a/src/p_user.c
+++ b/src/p_user.c
@@ -9185,9 +9185,12 @@ void P_PlayerThink(player_t *player)
 	if (player->powers[pw_underwater] && (player->pflags & PF_GODMODE || (player->powers[pw_shield] & SH_NOSTACK) == SH_ELEMENTAL))
 	{
 		if (player->powers[pw_underwater] <= 12*TICRATE+1)
+		{
+			player->powers[pw_underwater] = 0;
 			P_RestoreMusic(player); //incase they were about to drown
-
-		player->powers[pw_underwater] = 0;
+		}
+		else
+			player->powers[pw_underwater] = 0;
 	}
 	else if (player->powers[pw_underwater] && !(maptol & TOL_NIGHTS) && !((netgame || multiplayer) && player->spectator)) // underwater timer
 		player->powers[pw_underwater]--;

From f2033350fccf8bc512ab69ffcad2294ca55c6f68 Mon Sep 17 00:00:00 2001
From: mazmazz <mar.marcoz@outlook.com>
Date: Fri, 21 Sep 2018 15:53:03 -0400
Subject: [PATCH 10/10] MP Jingle: A brace (Underwater restore, p_inter.c)

---
 src/p_inter.c | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/p_inter.c b/src/p_inter.c
index 2c5c85ed3..796759514 100644
--- a/src/p_inter.c
+++ b/src/p_inter.c
@@ -1429,6 +1429,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 
 			// Eaten by player!
 			if (player->powers[pw_underwater] && player->powers[pw_underwater] <= 12*TICRATE + 1)
+			{
 				player->powers[pw_underwater] = underwatertics + 1;
 				P_RestoreMusic(player);
 			}