diff --git OpenMPT/build/android_ndk/Android-minimp3-stbvorbis.mk OpenMPT/build/android_ndk/Android-minimp3-stbvorbis.mk
index 7ec6552..4c0be60 100644
--- OpenMPT/build/android_ndk/Android-minimp3-stbvorbis.mk
+++ OpenMPT/build/android_ndk/Android-minimp3-stbvorbis.mk
@@ -73,6 +73,7 @@ LOCAL_SRC_FILES := \
 	soundlib/Load_psm.cpp \
 	soundlib/Load_ptm.cpp \
 	soundlib/Load_s3m.cpp \
+	soundlib/Load_sfx.cpp \
 	soundlib/Load_stm.cpp \
 	soundlib/Load_ult.cpp \
 	soundlib/Load_umx.cpp \
diff --git OpenMPT/build/android_ndk/Android-mpg123-vorbis.mk OpenMPT/build/android_ndk/Android-mpg123-vorbis.mk
index 0c09b5e..0b1d8e6 100644
--- OpenMPT/build/android_ndk/Android-mpg123-vorbis.mk
+++ OpenMPT/build/android_ndk/Android-mpg123-vorbis.mk
@@ -71,6 +71,7 @@ LOCAL_SRC_FILES := \
 	soundlib/Load_psm.cpp \
 	soundlib/Load_ptm.cpp \
 	soundlib/Load_s3m.cpp \
+	soundlib/Load_sfx.cpp \
 	soundlib/Load_stm.cpp \
 	soundlib/Load_ult.cpp \
 	soundlib/Load_umx.cpp \
diff --git OpenMPT/build/android_ndk/Android-unmo3.mk OpenMPT/build/android_ndk/Android-unmo3.mk
index f28d829..0f89ca3 100644
--- OpenMPT/build/android_ndk/Android-unmo3.mk
+++ OpenMPT/build/android_ndk/Android-unmo3.mk
@@ -104,6 +104,7 @@ LOCAL_SRC_FILES := \
 	soundlib/Load_psm.cpp \
 	soundlib/Load_ptm.cpp \
 	soundlib/Load_s3m.cpp \
+	soundlib/Load_sfx.cpp \
 	soundlib/Load_stm.cpp \
 	soundlib/Load_ult.cpp \
 	soundlib/Load_umx.cpp \
diff --git OpenMPT/build/android_ndk/Android.mk OpenMPT/build/android_ndk/Android.mk
index 6ef628b..f5214ca 100644
--- OpenMPT/build/android_ndk/Android.mk
+++ OpenMPT/build/android_ndk/Android.mk
@@ -72,6 +72,7 @@ LOCAL_SRC_FILES := \
 	soundlib/Load_psm.cpp \
 	soundlib/Load_ptm.cpp \
 	soundlib/Load_s3m.cpp \
+	soundlib/Load_sfx.cpp \
 	soundlib/Load_stm.cpp \
 	soundlib/Load_ult.cpp \
 	soundlib/Load_umx.cpp \
diff --git OpenMPT/build/autotools/Makefile.am OpenMPT/build/autotools/Makefile.am
index 86dbdd2..afdb965 100644
--- OpenMPT/build/autotools/Makefile.am
+++ OpenMPT/build/autotools/Makefile.am
@@ -184,6 +184,7 @@ libopenmpt_la_SOURCES += soundlib/Load_plm.cpp
 libopenmpt_la_SOURCES += soundlib/Load_psm.cpp
 libopenmpt_la_SOURCES += soundlib/Load_ptm.cpp
 libopenmpt_la_SOURCES += soundlib/Load_s3m.cpp
+libopenmpt_la_SOURCES += soundlib/Load_sfx.cpp
 libopenmpt_la_SOURCES += soundlib/Load_stm.cpp
 libopenmpt_la_SOURCES += soundlib/Load_ult.cpp
 libopenmpt_la_SOURCES += soundlib/Load_umx.cpp
@@ -417,6 +418,7 @@ libopenmpttest_SOURCES += soundlib/Load_plm.cpp
 libopenmpttest_SOURCES += soundlib/Load_psm.cpp
 libopenmpttest_SOURCES += soundlib/Load_ptm.cpp
 libopenmpttest_SOURCES += soundlib/Load_s3m.cpp
+libopenmpttest_SOURCES += soundlib/Load_sfx.cpp
 libopenmpttest_SOURCES += soundlib/Load_stm.cpp
 libopenmpttest_SOURCES += soundlib/Load_ult.cpp
 libopenmpttest_SOURCES += soundlib/Load_umx.cpp
diff --git OpenMPT/libopenmpt/foo_openmpt.cpp OpenMPT/libopenmpt/foo_openmpt.cpp
index ac7ce34..4517eff 100644
--- OpenMPT/libopenmpt/foo_openmpt.cpp
+++ OpenMPT/libopenmpt/foo_openmpt.cpp
@@ -319,6 +319,9 @@ DECLARE_FILE_TYPE("OpenMPT compatible module files",
 	"*.imf" ";"
 	"*.j2b" ";"
 	"*.plm" ";"
+	"*.sfx" ";"
+	"*.sfx2" ";"
+	"*.mms" ";"
 	"*.gdm" ";"
 	"*.umx" ";"
 	"*.mo3" ";"
diff --git OpenMPT/soundlib/Load_sfx.cpp OpenMPT/soundlib/Load_sfx.cpp
new file mode 100644
index 0000000..884dc8c
--- /dev/null
+++ OpenMPT/soundlib/Load_sfx.cpp
@@ -0,0 +1,381 @@
+/*
+ * Load_sfx.cpp
+ * ------------
+ * Purpose: SFX / MMS (SoundFX / MultiMedia Sound) module loader
+ * Notes  : Mostly based on the Soundtracker loader, some effect behavior is based on Flod's implementation.
+ * Authors: Devin Acker
+ *          OpenMPT Devs
+ * The OpenMPT source code is released under the BSD license. Read LICENSE for more details.
+ */
+
+#include "stdafx.h"
+#include "Loaders.h"
+#include "Tables.h"
+
+OPENMPT_NAMESPACE_BEGIN
+
+#ifdef NEEDS_PRAGMA_PACK
+#pragma pack(push, 1)
+#endif
+
+// File Header
+struct PACKED SFXFileHeader
+{
+	uint8 numOrders;
+	uint8 restartPos;
+	uint8 orderList[128];
+};
+
+STATIC_ASSERT(sizeof(SFXFileHeader) == 130);
+
+// Sample Header
+struct PACKED SFXSampleHeader
+{
+	char   name[22];
+	uint16 dummy; // supposedly sample length, but almost always incorrect
+	uint8  finetune;
+	uint8  volume;
+	uint16 loopStart;
+	uint16 loopLength;
+
+	// Convert all multi-byte numeric values to current platform's endianness or vice versa.
+	void ConvertEndianness()
+	{
+		SwapBytesBE(loopStart);
+		SwapBytesBE(loopLength);
+	}
+
+	// Convert an MOD sample header to OpenMPT's internal sample header.
+	void ConvertToMPT(ModSample &mptSmp, uint32 length) const
+	{
+		mptSmp.Initialize(MOD_TYPE_MOD);
+		mptSmp.nLength = static_cast<SmpLength>(length);
+		mptSmp.nFineTune = MOD2XMFineTune(finetune & 0x0F);
+		mptSmp.nVolume = 4 * MIN(volume, 64);
+
+		SmpLength lStart = loopStart;
+		SmpLength lLength = loopLength * 2;
+
+		if(mptSmp.nLength)
+		{
+			mptSmp.nLoopStart = lStart;
+			mptSmp.nLoopEnd = lStart + lLength;
+
+			if(mptSmp.nLoopStart >= mptSmp.nLength)
+			{
+				mptSmp.nLoopStart = mptSmp.nLength - 1;
+			}
+			if(mptSmp.nLoopEnd > mptSmp.nLength)
+			{
+				mptSmp.nLoopEnd = mptSmp.nLength;
+			}
+			if(mptSmp.nLoopStart > mptSmp.nLoopEnd || mptSmp.nLoopEnd < 4 || mptSmp.nLoopEnd - mptSmp.nLoopStart < 4)
+			{
+				mptSmp.nLoopStart = 0;
+				mptSmp.nLoopEnd = 0;
+			}
+
+			if(mptSmp.nLoopEnd > mptSmp.nLoopStart)
+			{
+				mptSmp.uFlags.set(CHN_LOOP);
+			}
+		}
+	}
+};
+
+STATIC_ASSERT(sizeof(SFXSampleHeader) == 30);
+
+#ifdef NEEDS_PRAGMA_PACK
+#pragma pack(pop)
+#endif
+
+static uint32 ReadSample(FileReader &file, SFXSampleHeader &sampleHeader, uint32 length, ModSample &sample, char (&sampleName)[MAX_SAMPLENAME])
+//------------------------------------------------------------------------------------------------------------------------------
+{
+	file.ReadConvertEndianness(sampleHeader);
+	sampleHeader.ConvertToMPT(sample, length);
+
+	mpt::String::Read<mpt::String::spacePadded>(sampleName, sampleHeader.name);
+	// Get rid of weird characters in sample names.
+	uint32 invalidChars = 0;
+	for(uint32 i = 0; i < CountOf(sampleName); i++)
+	{
+		if(sampleName[i] > 0 && sampleName[i] < ' ')
+		{
+			sampleName[i] = ' ';
+			invalidChars++;
+		}
+	}
+	return invalidChars;
+}
+
+bool CSoundFile::ReadSFX(FileReader &file, ModLoadingFlags loadFlags)
+//-------------------------------------------------------------------
+{
+	if(file.Seek(0x3C), file.ReadMagic("SONG"))
+	{
+		InitializeGlobals(MOD_TYPE_SFX);
+		m_nSamples = 15;
+	} else if(file.Seek(0x7C), file.ReadMagic("SO31"))
+	{
+		InitializeGlobals(MOD_TYPE_SFX);
+		m_nSamples = 31;
+	} else
+	{
+		return false;
+	}
+	
+	size_t totalSampleLen = 0;
+	uint32 sampleLen[31];
+	
+	file.Rewind();
+	for(SAMPLEINDEX smp = 0; smp < m_nSamples; smp++)
+	{
+		sampleLen[smp] = file.ReadUint32BE();
+		if(sampleLen[smp] > 65535)
+			return false;
+			
+		totalSampleLen += sampleLen[smp];
+	}
+	
+	m_nChannels = 4;
+	m_nInstruments = 0;
+	m_nDefaultSpeed = 6;
+	m_nMinPeriod = 14 * 4;
+	m_nMaxPeriod = 3424 * 4;
+	m_nSamplePreAmp = 64;
+	m_SongFlags.reset();
+
+	// Setup channel pan positions and volume
+	SetupMODPanning(true);
+	
+	file.Skip(4);
+	uint16 speed = file.ReadUint16BE();
+	m_nDefaultTempo = TEMPO(14565.0 * 122 / speed);
+	
+	file.Skip(14);
+	
+	for(SAMPLEINDEX smp = 1; smp <= m_nSamples; smp++)
+	{
+		SFXSampleHeader sampleHeader;
+		ReadSample(file, sampleHeader, sampleLen[smp-1], Samples[smp], m_szNames[smp]);
+	}
+	
+	SFXFileHeader fileHeader;
+	file.ReadStruct(fileHeader);
+
+	if(fileHeader.numOrders > 128)
+		return false;
+
+	PATTERNINDEX numPatterns = 0;
+	for(ORDERINDEX ord = 0; ord < fileHeader.numOrders; ord++)
+	{
+		numPatterns = MAX(numPatterns, fileHeader.orderList[ord]+1);
+	}
+	
+	if(loadFlags == onlyVerifyHeader)
+		return true;
+	
+	if(fileHeader.restartPos < fileHeader.numOrders)
+		Order.SetRestartPos(fileHeader.restartPos);
+	else
+		Order.SetRestartPos(0);
+	
+	Order.ReadFromArray(fileHeader.orderList);
+	Order.resize(fileHeader.numOrders);
+	
+	// SFX v2 / MMS modules have 4 extra bytes here for some reason
+	if(m_nSamples == 31)
+		file.Skip(4);
+	
+	uint8 lastNote[4] = {0};
+	uint8 slideTo[4] = {0};
+	uint8 slideRate[4] = {0};
+		
+	// Reading patterns
+	for(PATTERNINDEX pat = 0; pat < numPatterns; pat++)
+	{
+		if(!(loadFlags & loadPatternData) || !Patterns.Insert(pat, 64))
+		{
+			file.Skip(64 * 4 * 4);
+			continue;
+		}
+
+		for(ROWINDEX row = 0; row < 64; row++)
+		{
+			PatternRow rowBase = Patterns[pat].GetpModCommand(row, 0);
+			for(CHANNELINDEX chn = 0; chn < 4; chn++)
+			{
+				ModCommand &m = rowBase[chn];
+				uint8 data[4];
+				file.ReadArray(data);
+
+				if(data[0] == 0xFF)
+				{
+					m.note = NOTE_NONE;
+					m.instr = m.command = m.param = 0;
+					lastNote[chn] = slideRate[chn] = 0;
+					
+					switch(data[1])
+					{
+					case 0xFE: // STP (note cut)
+						m.command = CMD_VOLUME;
+						continue;
+						
+					case 0xFD: // PIC (null)
+						continue;
+						
+					case 0xFC: // BRK (pattern break)
+						m.command = CMD_PATTERNBREAK;
+						continue;
+					}
+				} 
+			
+				// Read Period
+				uint16 period = (((static_cast<uint16>(data[0]) & 0x0F) << 8) | data[1]);
+				m.note = NOTE_NONE;
+				if(period > 0 && period != 0xFFF)
+				{
+					m.note = 6 * 12 + 35 + NOTE_MIN;
+					for(int i = 0; i < 6 * 12; i++)
+					{
+						if(period >= ProTrackerPeriodTable[i])
+						{
+							if(period != ProTrackerPeriodTable[i] && i != 0)
+							{
+								uint16 p1 = ProTrackerPeriodTable[i - 1];
+								uint16 p2 = ProTrackerPeriodTable[i];
+								if(p1 - period < (period - p2))
+								{
+									m.note = static_cast<ModCommand::NOTE>(i + 35 + NOTE_MIN);
+									break;
+								}
+							}
+							m.note = static_cast<ModCommand::NOTE>(i + 36 + NOTE_MIN);
+							break;
+						}
+					}
+				}
+				// Read Instrument
+				m.instr = (data[2] >> 4) | (data[0] & 0x10);
+				// Read Effect
+				m.command = data[2] & 0x0F;
+				m.param = data[3];
+				
+				if(m.note)
+				{
+					lastNote[chn] = m.note;
+					slideRate[chn] = 0;
+				}
+				
+				if(m.command || m.param)
+				{
+					switch(m.command)
+					{
+					case 0x1: // arpeggio
+						m.command = CMD_ARPEGGIO;
+						break;
+						
+					case 0x2: // portamento (like Ultimate Soundtracker)
+						if(m.param & 0xF0)
+						{
+							m.command = CMD_PORTAMENTODOWN;
+							m.param >>= 4;
+						} else if(m.param & 0xF)
+						{
+							m.command = CMD_PORTAMENTOUP;
+							m.param &= 0x0F;
+						} else
+						{
+							m.command = m.param = 0;
+						}
+						break;
+						
+					case 0x3: // enable filter/LED
+						m.command = CMD_MODCMDEX;
+						m.param = 0;
+						break;
+						
+					case 0x4: // disable filter/LED
+						m.command = CMD_MODCMDEX;
+						m.param = 1;
+						break;
+						
+					case 0x5: // increase volume
+						if(m.instr)
+						{
+							m.command = CMD_VOLUME;
+							m.param = MIN(0x3F, (Samples[m.instr].nVolume >> 2) + m.param);
+						} else
+						{
+							m.command = m.param = 0;
+						}
+						break;
+					
+					case 0x6: // decrease volume
+						if(m.instr)
+						{
+							m.command = CMD_VOLUME;
+							if((Samples[m.instr].nVolume >> 2) >= m.param)
+								m.param = (Samples[m.instr].nVolume >> 2) - m.param;
+							else
+								m.param = 0;
+						} else
+						{
+							m.command = m.param = 0;
+						}
+						break;
+						
+					case 0x7: // 7xy: slide down x semitones at speed y
+						slideTo[chn] = lastNote[chn] - (m.param >> 4);
+						
+						m.command = CMD_PORTAMENTODOWN;
+						m.param = slideRate[chn] = m.param & 0xF;
+						break;
+						
+					case 0x8: // 8xy: slide up x semitones at speed y
+						slideTo[chn] = lastNote[chn] + (m.param >> 4);
+						
+						m.command = CMD_PORTAMENTOUP;
+						m.param = slideRate[chn] = m.param & 0xF;
+						break;
+					
+					default:
+						m.command = m.param = 0;
+						break;
+					}
+				}
+				
+				// continue 7xy/8xy slides if needed
+				if(!m.command && slideRate[chn])
+				{
+					if(slideTo[chn])
+					{
+						m.note = lastNote[chn] = slideTo[chn];
+						m.param = slideRate[chn];
+						slideTo[chn] = 0;
+					}
+					m.command = CMD_TONEPORTAMENTO;
+				}
+			}
+		}
+	}
+	
+	// Reading samples
+	if(loadFlags & loadSampleData)
+	{
+		for(SAMPLEINDEX smp = 1; smp <= m_nSamples; smp++) if(Samples[smp].nLength)
+		{
+			SampleIO(
+				SampleIO::_8bit,
+				SampleIO::mono,
+				SampleIO::littleEndian,
+				SampleIO::signedPCM)
+				.ReadSample(Samples[smp], file);
+		}
+	}
+	
+	return true;
+}
+
+OPENMPT_NAMESPACE_END
diff --git OpenMPT/soundlib/Snd_defs.h OpenMPT/soundlib/Snd_defs.h
index 4499f0b..398132e 100644
--- OpenMPT/soundlib/Snd_defs.h
+++ OpenMPT/soundlib/Snd_defs.h
@@ -97,6 +97,7 @@ enum MODTYPE
 	MOD_TYPE_DIGI	= 0x8000000,
 	MOD_TYPE_UAX	= 0x10000000, // sampleset as module
 	MOD_TYPE_PLM	= 0x20000000,
+	MOD_TYPE_SFX	= 0x40000000,
 };
 DECLARE_FLAGSET(MODTYPE)
 
diff --git OpenMPT/soundlib/Sndfile.cpp OpenMPT/soundlib/Sndfile.cpp
index d85039d..8c61101 100644
--- OpenMPT/soundlib/Sndfile.cpp
+++ OpenMPT/soundlib/Sndfile.cpp
@@ -313,6 +313,7 @@ bool CSoundFile::Create(FileReader file, ModLoadingFlags loadFlags)
 		 && !ReadMod(file, loadFlags)
 		 && !Read669(file, loadFlags)
 		 && !ReadICE(file, loadFlags)
+		 && !ReadSFX(file, loadFlags)
 		 && !ReadM15(file, loadFlags))
 		{
 			m_nType = MOD_TYPE_NONE;
@@ -1041,6 +1042,7 @@ MODTYPE CSoundFile::GetBestSaveFormat() const
 		return GetType();
 	case MOD_TYPE_AMF0:
 	case MOD_TYPE_DIGI:
+	case MOD_TYPE_SFX:
 		return MOD_TYPE_MOD;
 	case MOD_TYPE_MED:
 		if(m_nDefaultTempo == TEMPO(125, 0) && m_nDefaultSpeed == 6 && !m_nInstruments)
diff --git OpenMPT/soundlib/Sndfile.h OpenMPT/soundlib/Sndfile.h
index eee6e13..ab655e9 100644
--- OpenMPT/soundlib/Sndfile.h
+++ OpenMPT/soundlib/Sndfile.h
@@ -669,6 +669,7 @@ public:
 	bool ReadDIGI(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule);
 	bool ReadPLM(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule);
 	bool ReadMID(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule);
+	bool ReadSFX(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule);
 
 	static std::vector<const char *> GetSupportedExtensions(bool otherFormats);
 	static mpt::Charset GetCharsetFromModType(MODTYPE modtype);
diff --git OpenMPT/soundlib/Tables.cpp OpenMPT/soundlib/Tables.cpp
index abf91a9..ab1dd15 100644
--- OpenMPT/soundlib/Tables.cpp
+++ OpenMPT/soundlib/Tables.cpp
@@ -87,6 +87,9 @@ static const ModFormatInfo modFormatInfo[] =
 	{ MOD_TYPE_IMF,		"Imago Orpheus",			"imf" },
 	{ MOD_TYPE_J2B,		"Galaxy Sound System",		"j2b" },
 	{ MOD_TYPE_PLM,		"Disorder Tracker 2",		"plm" },
+	{ MOD_TYPE_SFX,		"SoundFX",					"sfx" },
+	{ MOD_TYPE_SFX,		"SoundFX",					"sfx2" },
+	{ MOD_TYPE_SFX,		"MultiMedia Sound",			"mms" },
 
 #ifndef NO_ARCHIVE_SUPPORT
 	// Compressed modules
@@ -147,6 +150,7 @@ static const ModCharsetInfo ModCharsetInfos[] =
 	{ MOD_TYPE_OKT , mpt::CharsetISO8859_1  },
 	{ MOD_TYPE_DBM , mpt::CharsetISO8859_1  },
 	{ MOD_TYPE_DIGI, mpt::CharsetISO8859_1  },
+	{ MOD_TYPE_SFX , mpt::CharsetISO8859_1  },
 	// Amiga // DOS
 	{ MOD_TYPE_MOD , mpt::CharsetISO8859_1  },
 	{ MOD_TYPE_MED , mpt::CharsetISO8859_1  },
