diff --git a/Config/Music.xml b/Config/Music.xml
new file mode 100644
index 0000000..d67bb58
--- /dev/null
+++ b/Config/Music.xml
@@ -0,0 +1,31 @@
+<!--
+  Supermodel
+  A Sega Model 3 Arcade Emulator.
+  Copyright 2003-2024 The Supermodel Team
+
+  Music.xml
+
+  This file defines custom MPEG music tracks to be used in DSB1 and DSB2 games.
+  Make sure to use MP3 files sampled at 32 KHz. Normal sampling rates (e.g. 44.1
+  or 48 KHz) will sound slow.
+
+  The example below is wrapped in a dummy <comment></comment> block to render it
+  inactive. Remove these and add your own MP3 files.
+-->
+<games>
+  <!-- Scud Race (Australian version) example -->
+  <comment>   <!-- Remove this tag -->
+  <game name="scudau">
+    <track mpeg_rom_start_offset="0x001d4b42" filepath="Rick Astley - Together Forever - 32 KHz.mp3" />         <!-- Beginner Day -->
+    <track mpeg_rom_start_offset="0x000e7e41" filepath="Rick Astley - Together Forever - 32 KHz.mp3" />         <!-- Beginner Night (initial start) -->
+    <track mpeg_rom_start_offset="0x00103981" filepath="Rick Astley - Together Forever - 32 KHz.mp3" />         <!-- Beginner Night (loop point) -->
+    <track mpeg_rom_start_offset="0x00000000" filepath="Rick Astley - Together Forever - 32 KHz.mp3" />         <!-- Medium (initial start) -->
+    <track mpeg_rom_start_offset="0x0001caff" filepath="Rick Astley - Together Forever - 32 KHz.mp3" />         <!-- Medium (loop point) -->
+    <track mpeg_rom_start_offset="0x0037a4c4" filepath="Rick Astley - Together Forever - 32 KHz.mp3" />         <!-- Expert (initial start) -->
+    <track mpeg_rom_start_offset="0x0037fc84" filepath="Rick Astley - Together Forever - 32 KHz.mp3" />         <!-- Expert (loop point) -->
+    <track mpeg_rom_start_offset="0x002afa83" filepath="Rick Astley - Together Forever - 32 KHz.mp3" />         <!-- Extra (initial start) -->
+    <track mpeg_rom_start_offset="0x002b4943" filepath="Rick Astley - Together Forever - 32 KHz.mp3" />         <!-- Extra (loop point) -->
+    <track mpeg_rom_start_offset="0x00463745" filepath="Rick Astley - Never Gonna Give You Up - 32 KHz.mp3" />  <!-- Selector -->
+  </game>
+  </comment>  <!-- Remove this tag! -->
+</games>
diff --git a/Src/Model3/DSB.cpp b/Src/Model3/DSB.cpp
index 9154383..24e4d11 100644
--- a/Src/Model3/DSB.cpp
+++ b/Src/Model3/DSB.cpp
@@ -221,7 +221,7 @@ void CDSB1::IOWrite8(UINT32 addr, UINT8 data)
 			usingMPEGStart	= mpegStart;
 			usingMPEGEnd	= mpegEnd;
 
-			MpegDec::SetMemory(&mpegROM[mpegStart], mpegEnd - mpegStart, false);
+			MpegDec::SetMemory(mpegROM, mpegStart, mpegEnd - mpegStart, false);
 			return;
 		}
 
@@ -233,7 +233,7 @@ void CDSB1::IOWrite8(UINT32 addr, UINT8 data)
 			usingMPEGStart	= mpegStart;
 			usingMPEGEnd	= mpegEnd;
 
-			MpegDec::SetMemory(&mpegROM[mpegStart], mpegEnd - mpegStart, false);		// assume not looped for now
+			MpegDec::SetMemory(mpegROM, mpegStart, mpegEnd - mpegStart, false);		// assume not looped for now
 			return;
 		}
 		break;
@@ -266,13 +266,13 @@ void CDSB1::IOWrite8(UINT32 addr, UINT8 data)
 			{
 				usingLoopStart	= loopStart;
 				usingLoopEnd	= mpegEnd-loopStart;
-				MpegDec::UpdateMemory(&mpegROM[usingLoopStart], usingLoopEnd, true);
+				MpegDec::UpdateMemory(mpegROM, usingLoopStart, usingLoopEnd, true);
 			}
 			else
 			{
 				usingLoopStart	= loopStart;
 				usingLoopEnd	= loopEnd-loopStart;
-				MpegDec::UpdateMemory(&mpegROM[usingLoopStart], usingLoopEnd, true);
+				MpegDec::UpdateMemory(mpegROM, usingLoopStart, usingLoopEnd, true);
 			}
 		}
 
@@ -302,7 +302,7 @@ void CDSB1::IOWrite8(UINT32 addr, UINT8 data)
 			loopEnd			= endLatch;
 			usingLoopStart	= loopStart;
 			usingLoopEnd	= loopEnd-loopStart;
-			MpegDec::UpdateMemory(&mpegROM[usingLoopStart], usingLoopEnd, true);
+			MpegDec::UpdateMemory(mpegROM, usingLoopStart, usingLoopEnd, true);
 			//printf("loopEnd = %08X\n", loopEnd);
 		}
 		break;
@@ -529,10 +529,10 @@ void CDSB1::LoadState(CBlockFile *StateFile)
 	// Restart MPEG audio at the appropriate position
 	if (isPlaying)
 	{
-		MpegDec::SetMemory(&mpegROM[usingMPEGStart], usingMPEGEnd - usingMPEGStart, false);
+		MpegDec::SetMemory(mpegROM, usingMPEGStart, usingMPEGEnd - usingMPEGStart, false);
 
 		if (usingLoopEnd != 0) {	// only if looping was actually enabled
-			MpegDec::UpdateMemory(&mpegROM[usingLoopStart], usingLoopEnd, true);
+			MpegDec::UpdateMemory(mpegROM, usingLoopStart, usingLoopEnd, true);
 		}
 
 		MpegDec::SetPosition(playOffset);
@@ -691,7 +691,7 @@ void CDSB2::WriteMPEGFIFO(UINT8 byte)
 				usingMPEGEnd	= mpegEnd;
 				playing			= 1;
 
-				MpegDec::SetMemory(&mpegROM[mpegStart], mpegEnd - mpegStart, false);
+				MpegDec::SetMemory(mpegROM, mpegStart, mpegEnd - mpegStart, false);
 
 				mpegState = ST_IDLE;
 			}
@@ -737,7 +737,7 @@ void CDSB2::WriteMPEGFIFO(UINT8 byte)
 			{
 				usingLoopStart	= mpegStart;
 				usingLoopEnd	= mpegEnd - mpegStart;
-				MpegDec::UpdateMemory(&mpegROM[usingLoopStart], usingLoopEnd, true);
+				MpegDec::UpdateMemory(mpegROM, usingLoopStart, usingLoopEnd, true);
 			}
 
 			break;
@@ -773,7 +773,7 @@ void CDSB2::WriteMPEGFIFO(UINT8 byte)
 				usingMPEGStart	= mpegStart;
 				usingMPEGEnd	= mpegEnd;
 				playing			= 1;
-				MpegDec::SetMemory(&mpegROM[mpegStart], mpegEnd - mpegStart, false);
+				MpegDec::SetMemory(mpegROM, mpegStart, mpegEnd - mpegStart, false);
 			}
 			break;
 		case ST_GOTA5:
@@ -1165,10 +1165,10 @@ void CDSB2::LoadState(CBlockFile *StateFile)
 	// Restart MPEG audio at the appropriate position
 	if (isPlaying)
 	{
-		MpegDec::SetMemory(&mpegROM[usingMPEGStart], usingMPEGEnd - usingMPEGStart, false);
+		MpegDec::SetMemory(mpegROM, usingMPEGStart, usingMPEGEnd - usingMPEGStart, false);
 
 		if (usingLoopEnd != 0) {		// only if looping was actually enabled
-			MpegDec::UpdateMemory(&mpegROM[usingLoopStart], usingLoopEnd, true);
+			MpegDec::UpdateMemory(mpegROM, usingLoopStart, usingLoopEnd, true);
 		}
 
 		MpegDec::SetPosition(playOffset);
diff --git a/Src/OSD/SDL/Main.cpp b/Src/OSD/SDL/Main.cpp
index dc37b66..7108536 100644
--- a/Src/OSD/SDL/Main.cpp
+++ b/Src/OSD/SDL/Main.cpp
@@ -1,7 +1,7 @@
 /**
  ** Supermodel
  ** A Sega Model 3 Arcade Emulator.
- ** Copyright 2003-2023 The Supermodel Team
+ ** Copyright 2003-2024 The Supermodel Team
  **
  ** This file is part of Supermodel.
  **
@@ -95,6 +95,7 @@
 #include "OSD/Audio.h"
 #include "Graphics/New3D/VBO.h"
 #include "Graphics/SuperAA.h"
+#include "Sound/MPEG/MpegAudio.h"
 
 #include <iostream>
 #include "Util/BMPFile.h"
@@ -105,6 +106,12 @@
  Global Run-time Config
 ******************************************************************************/
 
+static const std::string s_analysisPath = Util::Format() << FileSystemPath::GetPath(FileSystemPath::Analysis);
+static const std::string s_configFilePath = Util::Format() << FileSystemPath::GetPath(FileSystemPath::Config) << "Supermodel.ini";
+static const std::string s_gameXMLFilePath = Util::Format() << FileSystemPath::GetPath(FileSystemPath::Config) << "Games.xml";
+static const std::string s_musicXMLFilePath = Util::Format() << FileSystemPath::GetPath(FileSystemPath::Config) << "Music.xml";
+static const std::string s_logFilePath = Util::Format() << FileSystemPath::GetPath(FileSystemPath::Log) << "Supermodel.log";
+
 static Util::Config::Node s_runtime_config("Global");
 
 
@@ -914,6 +921,9 @@ int Supermodel(const Game &game, ROMSet *rom_set, IEmulator *Model3, CInputs *In
     return 1;
   *rom_set = ROMSet();  // free up this memory we won't need anymore
 
+  // Customized music for games with MPEG boards
+  MpegDec::LoadCustomTracks(s_musicXMLFilePath, game);
+
   // Load NVRAM
   LoadNVRAM(Model3);
 
@@ -982,7 +992,7 @@ int Supermodel(const Game &game, ROMSet *rom_set, IEmulator *Model3, CInputs *In
     goto QuitError;
   if (OKAY != Render3D->Init(xOffset*aaValue, yOffset*aaValue, xRes*aaValue, yRes*aaValue, totalXRes*aaValue, totalYRes*aaValue, superAA->GetTargetID()))
     goto QuitError;
- 
+
   Model3->AttachRenderers(Render2D,Render3D, superAA);
 
   // Reset emulator
@@ -1356,12 +1366,6 @@ QuitError:
 /******************************************************************************
  Entry Point and Command Line Procesing
 ******************************************************************************/
-
-static const std::string s_analysisPath = Util::Format() << FileSystemPath::GetPath(FileSystemPath::Analysis);
-static const std::string s_configFilePath = Util::Format() << FileSystemPath::GetPath(FileSystemPath::Config) << "Supermodel.ini";
-static const std::string s_gameXMLFilePath = Util::Format() << FileSystemPath::GetPath(FileSystemPath::Config) << "Games.xml";
-static const std::string s_logFilePath = Util::Format() << FileSystemPath::GetPath(FileSystemPath::Log) << "Supermodel.log";
-
 // Create and configure inputs
 static bool ConfigureInputs(CInputs *Inputs, Util::Config::Node *fileConfig, Util::Config::Node *runtimeConfig, const Game &game, bool configure)
 {
@@ -1543,7 +1547,7 @@ static Util::Config::Node DefaultConfig()
 static void Title(void)
 {
   puts("Supermodel: A Sega Model 3 Arcade Emulator (Version " SUPERMODEL_VERSION ")");
-  puts("Copyright 2003-2023 by The Supermodel Team");
+  puts("Copyright 2003-2024 by The Supermodel Team");
 }
 
 static void Help(void)
diff --git a/Src/Sound/MPEG/MpegAudio.cpp b/Src/Sound/MPEG/MpegAudio.cpp
index 45e8c68..66f8fad 100644
--- a/Src/Sound/MPEG/MpegAudio.cpp
+++ b/Src/Sound/MPEG/MpegAudio.cpp
@@ -1,6 +1,205 @@
+/**
+ ** Supermodel
+ ** A Sega Model 3 Arcade Emulator.
+ ** Copyright 2003-2024 The Supermodel Team
+ **
+ ** This file is part of Supermodel.
+ **
+ ** Supermodel is free software: you can redistribute it and/or modify it under
+ ** the terms of the GNU General Public License as published by the Free
+ ** Software Foundation, either version 3 of the License, or (at your option)
+ ** any later version.
+ **
+ ** Supermodel is distributed in the hope that it will be useful, but WITHOUT
+ ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ ** FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
+ ** more details.
+ **
+ ** You should have received a copy of the GNU General Public License along
+ ** with Supermodel.  If not, see <http://www.gnu.org/licenses/>.
+ **/
+
 #define MINIMP3_IMPLEMENTATION
 #include "Pkgs/minimp3.h"
 #include "MpegAudio.h"
+#include "Util/ConfigBuilders.h"
+#include "OSD/Logger.h"
+
+#include <cstdio>
+#include <map>
+#include <memory>
+#include <filesystem>
+#include <tuple>
+
+
+/***************************************************************************************************
+ Custom MPEG Tracks
+***************************************************************************************************/
+
+struct CustomTrack
+{
+  std::shared_ptr<uint8_t[]> mpeg_data;
+  size_t mpeg_data_size;
+  uint32_t mpeg_rom_start_offset;
+  size_t file_start_offset;
+
+  CustomTrack()
+    : mpeg_data(nullptr),
+      mpeg_data_size(0),
+      mpeg_rom_start_offset(0),
+      file_start_offset(0)
+  {
+  }
+
+  CustomTrack(const std::shared_ptr<uint8_t[]> &mpeg_data, size_t mpeg_data_size, uint32_t mpeg_rom_start_offset, size_t file_start_offset)
+    : mpeg_data(mpeg_data),
+      mpeg_data_size(mpeg_data_size),
+      mpeg_rom_start_offset(mpeg_rom_start_offset),
+      file_start_offset(file_start_offset)
+  {
+  }
+};
+
+struct FileContents
+{
+  std::shared_ptr<uint8_t[]> bytes;
+  size_t size;
+};
+
+static std::map<uint32_t, CustomTrack> s_custom_tracks_by_mpeg_rom_address;
+
+static FileContents LoadFile(const std::string &filepath)
+{
+  FILE *fp = fopen(filepath.c_str(), "rb");
+  if (!fp)
+  {
+    ErrorLog("Unable to load music track from disk: %s.", filepath.c_str());
+    return { .bytes = nullptr, .size = 0 };
+  }
+  fseek(fp, 0, SEEK_END);
+  long file_size = ftell(fp);
+  fseek(fp, 0, SEEK_SET);
+  std::shared_ptr<uint8_t[]> mpeg_data(new uint8_t[file_size], std::default_delete<uint8_t[]>());
+  fread(mpeg_data.get(), sizeof(uint8_t), file_size, fp);
+  fclose(fp);
+  return { .bytes = mpeg_data, .size = size_t(file_size) };
+}
+
+void MpegDec::LoadCustomTracks(const std::string &music_filepath, const Game &game)
+{
+  s_custom_tracks_by_mpeg_rom_address.clear();
+
+  if (game.mpeg_board.empty())
+  {
+    // No MPEG board
+    return;
+  }
+
+  if (!std::filesystem::exists(music_filepath))
+  {
+    // Custom music configuration file is optional
+    return;
+  }
+
+  Util::Config::Node xml("xml");
+  if (Util::Config::FromXMLFile(&xml, music_filepath))
+  {
+    ErrorLog("Custom music configuration file could not be loaded. Original game tracks will be used.");
+    return;
+  }
+
+  /*
+   * Sample XML:
+   *
+   * <games>
+   *  <game name="scud">
+   *    <track mpeg_rom_start_offset="" file_start_offset="0" filepath="song1.mp3" />
+   *    <track mpeg_rom_start_offset="" file_start_offset="0x1000" filepath="song2.mp3" />
+   *  </game>
+   * </games>
+   */
+
+  std::map<std::string, FileContents> file_contents_by_filepath;
+
+  for (auto it = xml.begin(); it != xml.end(); ++it)
+  {
+    auto &root_node = *it;
+    if (root_node.Key() != "games")
+    {
+      continue;
+    }
+
+    for (auto &game_node: root_node)
+    {
+      if (game_node.Key() != "game")
+      {
+        continue;
+      }
+
+      if (game_node["name"].Empty())
+      {
+        continue;
+      }
+      std::string game_name = game_node["name"].ValueAs<std::string>();
+      if (game_name != game.name)
+      {
+        continue;
+      }
+
+      for (auto &track_node: game_node)
+      {
+        if (track_node.Key() != "track")
+        {
+          continue;
+        }
+
+        size_t file_start_offset = 0;
+        if (track_node["mpeg_rom_start_offset"].Empty())
+        {
+          ErrorLog("%s: Track in '%s' is missing 'mpeg_rom_start_offset' attribute and will be ignored.", music_filepath.c_str(), game.name.c_str());
+          continue;
+        }
+        if (track_node["filepath"].Empty())
+        {
+          ErrorLog("%s: Track in '%s' is missing 'filepath' attribute and will be ignored.", music_filepath.c_str(), game.name.c_str());
+          continue;
+        }
+        if (track_node["file_start_offset"].Exists())
+        {
+          file_start_offset = track_node["file_start_offset"].ValueAs<size_t>();
+        }
+        const std::string filepath = track_node["filepath"].ValueAs<std::string>();
+        const uint32_t mpeg_rom_start_offset = track_node["mpeg_rom_start_offset"].ValueAs<uint32_t>();
+
+        if (s_custom_tracks_by_mpeg_rom_address.count(mpeg_rom_start_offset) != 0)
+        {
+          ErrorLog("%s: Multiple tracks defined for '%s' MPEG ROM offset 0x%08x. Only the first will be used.", music_filepath.c_str(), game.name.c_str(), mpeg_rom_start_offset);
+          continue;
+        }
+
+        if (file_contents_by_filepath.count(filepath) == 0)
+        {
+          FileContents contents = LoadFile(filepath);
+          if (contents.bytes == nullptr)
+          {
+            continue;
+          }
+          file_contents_by_filepath[filepath] = contents;
+          InfoLog("Loaded custom track: %s.", filepath.c_str());
+          printf("Loaded custom track: %s.\n", filepath.c_str());
+        }
+
+        FileContents contents = file_contents_by_filepath[filepath];
+        s_custom_tracks_by_mpeg_rom_address[mpeg_rom_start_offset] = CustomTrack(contents.bytes, contents.size, mpeg_rom_start_offset, file_start_offset);
+      }
+    }
+  }
+}
+
+
+/***************************************************************************************************
+ MPEG Music Playback
+***************************************************************************************************/
 
 struct Decoder
 {
@@ -13,37 +212,95 @@ struct Decoder
 	int					numSamples;
 	int					pcmPos;
 	short				pcm[MINIMP3_MAX_SAMPLES_PER_FRAME];
+
+	std::shared_ptr<uint8_t[]>  custom_mpeg_data;
 };
 
 static Decoder dec = { 0 };
 
-void MpegDec::SetMemory(const uint8_t *data, int length, bool loop)
+void MpegDec::SetMemory(const uint8_t *data, int offset, int length, bool loop)
 {
 	mp3dec_init(&dec.mp3d);
 
-	dec.buffer		= data;
-	dec.size		= length;
-	dec.pos			= 0;
-	dec.numSamples	= 0;
-	dec.pcmPos		= 0;
-	dec.loop		= loop;
-	dec.stopped		= false;
+  auto it = s_custom_tracks_by_mpeg_rom_address.find(offset);
+  if (it == s_custom_tracks_by_mpeg_rom_address.end()) {
+    // MPEG ROM
+    dec.buffer            = data + offset;
+    dec.size              = length;
+    dec.custom_mpeg_data  = nullptr;
+  }
+  else {
+    // Custom track available
+    const CustomTrack &track = it->second;
+
+    size_t offset_in_file = track.file_start_offset;
+    if (offset_in_file >= track.mpeg_data_size)
+    {
+      // Out of bounds, go to start of file
+      offset_in_file = 0;
+    }
+
+    dec.buffer            = track.mpeg_data.get() + offset_in_file;
+    dec.size              = track.mpeg_data_size - offset_in_file;
+    dec.custom_mpeg_data  = track.mpeg_data;
+  }
+
+  dec.pos         = 0;
+  dec.numSamples  = 0;
+  dec.pcmPos      = 0;
+  dec.loop        = loop;
+  dec.stopped     = false;
+
+	printf("SET MEMORY: %08x\n", offset);
 }
 
-void MpegDec::UpdateMemory(const uint8_t* data, int length, bool loop)
+void MpegDec::UpdateMemory(const uint8_t* data, int offset, int length, bool loop)
 {
-	int diff;
-	if (data > dec.buffer) {
-		diff = (int)(data - dec.buffer);
-	}
-	else {
-		diff = -(int)(dec.buffer - data);
-	}
+  auto it = s_custom_tracks_by_mpeg_rom_address.find(offset);
+  if (it == s_custom_tracks_by_mpeg_rom_address.end()) {
+    // MPEG ROM
+  	int diff;
+  	if ((data + offset) > dec.buffer) {
+  		diff = (int)(data + offset - dec.buffer);
+  	}
+  	else {
+  		diff = -(int)(dec.buffer - data - offset);
+  	}
+    dec.buffer  = data + offset;
+    dec.size    = length;
+    dec.pos     = dec.pos - diff; // update position relative to our new start location
+  }
+  else {
+    // Custom track available. This is tricky. This command updates the start/end pointers (usually
+    // used by games to create a loop point). We need to ensure that the custom track definition is
+    // consistent: the custom track associated with this ROM offset must be the same file as is
+    // currently playing, otherwise we do nothing.
+    CustomTrack &track = it->second;
+    if (track.mpeg_data == dec.custom_mpeg_data)
+    {
+      size_t offset_in_file = track.file_start_offset;
+      if (offset_in_file >= track.mpeg_data_size)
+      {
+        // Out of bounds, just use start of file
+        offset_in_file = 0;
+      }
+
+      int diff;
+    	if ((track.mpeg_data.get() + offset_in_file) > dec.buffer) {
+    		diff = (int)(track.mpeg_data.get() + offset_in_file - dec.buffer);
+    	}
+    	else {
+    		diff = -(int)(dec.buffer - track.mpeg_data.get() - offset_in_file);
+    	}
+      dec.buffer  = track.mpeg_data.get() + offset_in_file;
+      dec.size    = track.mpeg_data_size - offset_in_file;  // ignoring length specified by caller because MPEG ROM end offsets won't in general match with track, so we always have to use EOF
+      dec.pos     = dec.pos - diff; // update position relative to our new start location
+    }
+  }
 
-	dec.buffer	= data;
-	dec.size	= length;
-	dec.pos		= dec.pos - diff;		// update position relative to our new start location
 	dec.loop	= loop;
+
+	printf("UPDATE MEMORY: %08x\n", offset);
 }
 
 int MpegDec::GetPosition()
diff --git a/Src/Sound/MPEG/MpegAudio.h b/Src/Sound/MPEG/MpegAudio.h
index 2a5b156..d77755c 100644
--- a/Src/Sound/MPEG/MpegAudio.h
+++ b/Src/Sound/MPEG/MpegAudio.h
@@ -1,12 +1,15 @@
 #ifndef _MPEG_AUDIO_H_
 #define _MPEG_AUDIO_H_
 
+#include "Game.h"
 #include <cstdint>
 
+
 namespace MpegDec
 {
-	void	SetMemory(const uint8_t *data, int length, bool loop);
-	void	UpdateMemory(const uint8_t *data, int length, bool loop);
+  void  LoadCustomTracks(const std::string &music_filepath, const Game &game);
+  void	SetMemory(const uint8_t *data, int offset, int length, bool loop);
+	void	UpdateMemory(const uint8_t *data, int offset, int length, bool loop);
 	int		GetPosition();
 	void	SetPosition(int pos);
 	void	DecodeAudio(int16_t* left, int16_t* right, int numStereoSamples);