View Issue Details

IDProjectCategoryView StatusLast Update
0000752OpenMPTFile Format Supportpublic2016-03-18 19:04
ReporterRevenant Assigned ToSaga Musix  
PrioritynormalSeverityminorReproducibilityN/A
Status resolvedResolutionfixed 
Product VersionOpenMPT 1.25.04.00 / libopenmpt 0.2-beta16 (upgrade first) 
Target VersionOpenMPT 1.26.01.00 / libopenmpt 0.2-beta17 (upgrade first)Fixed in VersionOpenMPT 1.26.01.00 / libopenmpt 0.2-beta17 (upgrade first) 
Summary0000752: ProTracker 3.6
Description

For some reason or another, ProTracker 3.6x supports saving modules inside of IFF containers. Aside from the very small amount of extra metadata, the format is basically identical, so I wrote a patch to load them (with the regular .mod loader still doing nearly all of the actual work).

There are a whopping 15 of these on Modland, but I attached 3 as examples.

(Also not really related but I noticed that the option to associate .st26 files was missing from the install script so I threw that in too)

TagsNo tags attached.
Attached Files
pt36.patch (8,483 bytes)   
diff --git OpenMPT/installer/filetypes.iss OpenMPT/installer/filetypes.iss
index d9826e4..057a170 100644
--- OpenMPT/installer/filetypes.iss
+++ OpenMPT/installer/filetypes.iss
@@ -40,7 +40,9 @@ Name: "associate_exotic\mtm"; Description: "MultiTracker Modules (MTM)";
 Name: "associate_exotic\okt"; Description: "Oktalyzer (OKT)";
 Name: "associate_exotic\plm"; Description: "Disorder Tracker 2 (PLM)";
 Name: "associate_exotic\psm"; Description: "Epic Megagames MASI (PSM)";
+Name: "associate_exotic\pt36"; Description: "ProTracker 3.6 (PT36)";
 Name: "associate_exotic\ptm"; Description: "PolyTracker (PTM)";
+Name: "associate_exotic\st26"; Description: "SoundTracker 2.6 (ST26)";
 Name: "associate_exotic\stm"; Description: "Scream Tracker 2 (STM)";
 Name: "associate_exotic\ult"; Description: "UltraTracker (ULT)";
 Name: "associate_exotic\umx"; Description: "Unreal Music (UMX)";
@@ -83,7 +85,9 @@ Root: HKCR; Subkey: ".mtm"; ValueType: string; ValueName: ""; ValueData: "OpenMP
 Root: HKCR; Subkey: ".okt"; ValueType: string; ValueName: ""; ValueData: "OpenMPTFile"; Flags: uninsdeletevalue; Tasks: associate_exotic\okt
 Root: HKCR; Subkey: ".plm"; ValueType: string; ValueName: ""; ValueData: "OpenMPTFile"; Flags: uninsdeletevalue; Tasks: associate_exotic\plm
 Root: HKCR; Subkey: ".psm"; ValueType: string; ValueName: ""; ValueData: "OpenMPTFile"; Flags: uninsdeletevalue; Tasks: associate_exotic\psm
+Root: HKCR; Subkey: ".pt36"; ValueType: string; ValueName: ""; ValueData: "OpenMPTFile"; Flags: uninsdeletevalue; Tasks: associate_exotic\pt36
 Root: HKCR; Subkey: ".ptm"; ValueType: string; ValueName: ""; ValueData: "OpenMPTFile"; Flags: uninsdeletevalue; Tasks: associate_exotic\ptm
+Root: HKCR; Subkey: ".st26"; ValueType: string; ValueName: ""; ValueData: "OpenMPTFile"; Flags: uninsdeletevalue; Tasks: associate_exotic\st26
 Root: HKCR; Subkey: ".stm"; ValueType: string; ValueName: ""; ValueData: "OpenMPTFile"; Flags: uninsdeletevalue; Tasks: associate_exotic\stm
 Root: HKCR; Subkey: ".ult"; ValueType: string; ValueName: ""; ValueData: "OpenMPTFile"; Flags: uninsdeletevalue; Tasks: associate_exotic\ult
 Root: HKCR; Subkey: ".umx"; ValueType: string; ValueName: ""; ValueData: "OpenMPTFile"; Flags: uninsdeletevalue; Tasks: associate_exotic\umx
diff --git OpenMPT/libopenmpt/foo_openmpt.cpp OpenMPT/libopenmpt/foo_openmpt.cpp
index 814a8ae..ac7ce34 100644
--- OpenMPT/libopenmpt/foo_openmpt.cpp
+++ OpenMPT/libopenmpt/foo_openmpt.cpp
@@ -295,6 +295,7 @@ DECLARE_FILE_TYPE("OpenMPT compatible module files",
 	"*.m15" ";"
 	"*.stk" ";"
 	"*.st26" ";"
+	"*.pt36" ";"
 	"*.ice" ";"
 	"*.wow" ";"
 	"*.ult" ";"
diff --git OpenMPT/mptrack/Mptrack.cpp OpenMPT/mptrack/Mptrack.cpp
index 4ba9aac..5aff1aa 100644
--- OpenMPT/mptrack/Mptrack.cpp
+++ OpenMPT/mptrack/Mptrack.cpp
@@ -1377,7 +1377,7 @@ void CTrackApp::OpenModulesDialog(std::vector<mpt::PathString> &files)
 		";*.mo3"
 #endif
 		"|"
-		"ProTracker Modules (*.mod,*.nst)|*.mod;mod.*;*.mdz;*.nst;*.m15;*.stk|"
+		"ProTracker Modules (*.mod,*.nst)|*.mod;mod.*;*.mdz;*.nst;*.m15;*.stk;*.pt36|"
 		"ScreamTracker Modules (*.s3m,*.stm)|*.s3m;*.stm;*.s3z|"
 		"FastTracker Modules (*.xm)|*.xm;*.xmz|"
 		"Impulse Tracker Modules (*.it)|*.it;*.itz|"
diff --git OpenMPT/soundlib/Load_mod.cpp OpenMPT/soundlib/Load_mod.cpp
index 02793ff..1aa8ab7 100644
--- OpenMPT/soundlib/Load_mod.cpp
+++ OpenMPT/soundlib/Load_mod.cpp
@@ -334,6 +334,29 @@ struct PACKED MODSampleHeader
 
 STATIC_ASSERT(sizeof(MODSampleHeader) == 30);
 
+struct PACKED PT36IffChunk
+{
+	// IFF chunk names
+	enum ChunkIdentifiers
+	{
+		idVERS	= MAGIC4BE('V','E','R','S'),
+		idINFO	= MAGIC4BE('I','N','F','O'),
+		idCMNT	= MAGIC4BE('C','M','N','T'),
+		idPTDT	= MAGIC4BE('P','T','D','T'),
+	};
+
+	uint32 signature;	// IFF chunk name
+	uint32 chunksize;	// chunk size without header
+
+	// Convert all multi-byte numeric values to current platform's endianness or vice versa.
+	void ConvertEndianness()
+	{
+		SwapBytesBE(signature);
+		SwapBytesBE(chunksize);
+	}
+};
+
+STATIC_ASSERT(sizeof(PT36IffChunk) == 8);
 
 #ifdef NEEDS_PRAGMA_PACK
 #pragma pack(pop)
@@ -490,6 +513,7 @@ bool CSoundFile::ReadMod(FileReader &file, ModLoadingFlags loadFlags)
 	// Check MOD Magic
 	if(IsMagic(magic, "M.K.")		// ProTracker and compatible
 		|| IsMagic(magic, "M!K!")	// ProTracker (64+ patterns)
+		|| IsMagic(magic, "PATT")	// ProTracker 3.6
 		|| IsMagic(magic, "NSMS")	// kingdomofpleasure.mod by bee hunter
 		|| IsMagic(magic, "LARD"))	// judgement_day_gvine.mod by 4-mat
 	{
@@ -1388,6 +1412,89 @@ bool CSoundFile::ReadICE(FileReader &file, ModLoadingFlags loadFlags)
 }
 
 
+
+// ProTracker 3.6 version of the MOD format
+// Basically just a normal ProTracker mod with different magic, wrapped in an IFF file.
+// The "PTDT" chunk is passed to the normal MOD loader.
+bool CSoundFile::ReadPT36(FileReader &file, ModLoadingFlags loadFlags)
+//-------------------------------------------------------------------
+{
+	file.Rewind();
+	if(!file.ReadMagic("FORM"))
+	{
+		return false;
+	}
+	
+	file.Skip(4);
+	if(!file.ReadMagic("MODL"))
+	{
+		return false;
+	}
+	
+	bool ok = false;
+	std::string title, message;
+	std::string version = "3.6";
+
+	// Go through IFF chunks...
+	PT36IffChunk iffHead;
+	if(!file.ReadConvertEndianness(iffHead))
+	{
+		return false;
+	}
+	// first chunk includes "MODL" magic in size
+	iffHead.chunksize -= 4;
+	
+	do 
+	{
+		// all chunk sizes include chunk header
+		iffHead.chunksize -= 8;
+		
+		FileReader chunk = file.ReadChunk(iffHead.chunksize);
+		if(!chunk.IsValid())
+		{
+			break;
+		}
+
+		switch(iffHead.signature)
+		{
+		case PT36IffChunk::idVERS:
+			chunk.Skip(4);
+			if(chunk.ReadMagic("PT"))
+			{
+				chunk.ReadString<mpt::String::maybeNullTerminated>(version, iffHead.chunksize - 6);
+			}
+			break;
+		
+		case PT36IffChunk::idINFO:
+			chunk.ReadString<mpt::String::maybeNullTerminated>(title, iffHead.chunksize);
+			break;
+		
+		case PT36IffChunk::idCMNT:
+			chunk.ReadString<mpt::String::maybeNullTerminated>(message, iffHead.chunksize);
+			break;
+		
+		case PT36IffChunk::idPTDT:
+			ok = ReadMod(chunk, loadFlags);
+			break;
+		}
+	} while(file.CanRead(sizeof(PT36IffChunk)) && file.ReadConvertEndianness(iffHead));
+	
+	if(ok)
+	{
+		if(!title.empty())
+			m_songName = title;
+	
+		// "message" chunk seems to only be used to store the artist name, despite being pretty long
+		if(message != "UNNAMED AUTHOR")
+			m_songArtist = mpt::ToUnicode(mpt::CharsetISO8859_1, message);
+		
+		m_madeWithTracker = "ProTracker ";
+		m_madeWithTracker += version;
+	}
+	
+	return ok;
+}
+
 #ifndef MODPLUG_NO_FILESAVE
 
 bool CSoundFile::SaveMod(const mpt::PathString &filename) const
diff --git OpenMPT/soundlib/Sndfile.cpp OpenMPT/soundlib/Sndfile.cpp
index 8930a6e..6672dfe 100644
--- OpenMPT/soundlib/Sndfile.cpp
+++ OpenMPT/soundlib/Sndfile.cpp
@@ -316,7 +316,8 @@ bool CSoundFile::Create(FileReader file, ModLoadingFlags loadFlags)
 		 && !ReadMod(file, loadFlags)
 		 && !Read669(file, loadFlags)
 		 && !ReadICE(file, loadFlags)
-		 && !ReadM15(file, loadFlags))
+		 && !ReadM15(file, loadFlags)
+		 && !ReadPT36(file, loadFlags))
 		{
 			m_nType = MOD_TYPE_NONE;
 			m_ContainerType = MOD_CONTAINERTYPE_NONE;
diff --git OpenMPT/soundlib/Sndfile.h OpenMPT/soundlib/Sndfile.h
index 4223d23..6ab175d 100644
--- OpenMPT/soundlib/Sndfile.h
+++ OpenMPT/soundlib/Sndfile.h
@@ -619,6 +619,7 @@ public:
 	bool ReadMod(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule);
 	bool ReadM15(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule);
 	bool ReadICE(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule);
+	bool ReadPT36(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule);
 	bool ReadMed(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule);
 	bool ReadMTM(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule);
 	bool ReadSTM(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule);
diff --git OpenMPT/soundlib/Tables.cpp OpenMPT/soundlib/Tables.cpp
index f446311..93ec4b3 100644
--- OpenMPT/soundlib/Tables.cpp
+++ OpenMPT/soundlib/Tables.cpp
@@ -59,6 +59,7 @@ static const ModFormatInfo modFormatInfo[] =
 #endif
 	{ MOD_TYPE_MPT,		"OpenMPT",					"mptm" },
 	{ MOD_TYPE_STM,		"ScreamTracker 2",			"stm" },
+	{ MOD_TYPE_MOD,		"ProTracker",				"pt36" },
 	{ MOD_TYPE_MOD,		"NoiseTracker",				"nst" },
 	{ MOD_TYPE_MOD,		"Soundtracker",				"m15" },
 	{ MOD_TYPE_MOD,		"Soundtracker",				"stk" },
pt36.patch (8,483 bytes)   
Protracker 3.6.rar (367,636 bytes)
pt36info.patch (3,172 bytes)   
diff --git OpenMPT/soundlib/Load_mod.cpp OpenMPT/soundlib/Load_mod.cpp
index 26e12a2..757a3c5 100644
--- OpenMPT/soundlib/Load_mod.cpp
+++ OpenMPT/soundlib/Load_mod.cpp
@@ -358,6 +358,50 @@ struct PACKED PT36IffChunk
 
 STATIC_ASSERT(sizeof(PT36IffChunk) == 8);
 
+struct PACKED PT36InfoChunk
+{
+	char name[32];
+	uint16 numSamples;  // unused
+	uint16 numOrders;   // unused
+	uint16 numPatterns; // unused
+	uint16 volume;
+	uint16 tempo;
+	uint16 flags; // unused
+	uint16 dateDay;
+	uint16 dateMonth;
+	uint16 dateYear;
+	uint16 dateHour;
+	uint16 dateMinute;
+	uint16 dateSecond;
+	uint16 playtimeHour;    // unused
+	uint16 playtimeMinute;  // unused
+	uint16 playtimeSecond;  // unused
+	uint16 playtimeMsecond; // unused
+
+	// Convert all multi-byte numeric values to current platform's endianness or vice versa.
+	void ConvertEndianness()
+	{
+		SwapBytesBE(numSamples);
+		SwapBytesBE(numOrders);
+		SwapBytesBE(numPatterns);
+		SwapBytesBE(volume);
+		SwapBytesBE(tempo);
+		SwapBytesBE(flags);
+		SwapBytesBE(dateDay);
+		SwapBytesBE(dateMonth);
+		SwapBytesBE(dateYear);
+		SwapBytesBE(dateHour);
+		SwapBytesBE(dateMinute);
+		SwapBytesBE(dateSecond);
+		SwapBytesBE(playtimeHour);
+		SwapBytesBE(playtimeMinute);
+		SwapBytesBE(playtimeSecond);
+		SwapBytesBE(playtimeMsecond);
+	}
+};
+
+STATIC_ASSERT(sizeof(PT36InfoChunk) == 64);
+
 #ifdef NEEDS_PRAGMA_PACK
 #pragma pack(pop)
 #endif
@@ -1441,9 +1485,10 @@ bool CSoundFile::ReadPT36(FileReader &file, ModLoadingFlags loadFlags)
 		return false;
 	}
 	
-	bool ok = false;
+	bool ok = false, infoOk = false;
 	std::string title, message;
 	std::string version = "3.6";
+	PT36InfoChunk info;
 
 	// Go through IFF chunks...
 	PT36IffChunk iffHead;
@@ -1480,7 +1525,7 @@ bool CSoundFile::ReadPT36(FileReader &file, ModLoadingFlags loadFlags)
 			break;
 		
 		case PT36IffChunk::idINFO:
-			chunk.ReadString<mpt::String::maybeNullTerminated>(title, iffHead.chunksize);
+			infoOk = chunk.ReadConvertEndianness(info);
 			break;
 		
 		case PT36IffChunk::idCMNT:
@@ -1493,11 +1538,26 @@ bool CSoundFile::ReadPT36(FileReader &file, ModLoadingFlags loadFlags)
 		}
 	} while(file.CanRead(sizeof(PT36IffChunk)) && file.ReadConvertEndianness(iffHead));
 	
-	if(ok)
+	// both an info chunk and a module are required
+	if(ok && infoOk)
 	{
+		m_nSamplePreAmp = MIN(64, info.volume);
+		m_nDefaultTempo.Set(info.tempo);
+	
+		mpt::String::Read<mpt::String::maybeNullTerminated>(title, info.name);
 		if(!title.empty())
 			m_songName = title;
 	
+		FileHistory mptHistory;
+		MemsetZero(mptHistory.loadDate);
+		mptHistory.loadDate.tm_year = info.dateYear;
+		mptHistory.loadDate.tm_mon = Clamp(info.dateMonth, 1, 12) - 1;
+		mptHistory.loadDate.tm_mday = Clamp(info.dateDay, 1, 31);
+		mptHistory.loadDate.tm_hour = Clamp(info.dateHour, 0, 23);
+		mptHistory.loadDate.tm_min = Clamp(info.dateMinute, 0, 59);
+		mptHistory.loadDate.tm_sec = Clamp(info.dateSecond, 0, 59);
+		m_FileHistory.push_back(mptHistory);
+	
 		// "message" chunk seems to only be used to store the artist name, despite being pretty long
 		if(message != "UNNAMED AUTHOR")
 			m_songArtist = mpt::ToUnicode(mpt::CharsetISO8859_1, message);
pt36info.patch (3,172 bytes)   
Has the bug occurred in previous versions?
Tested code revision (in case you know it)

Activities

Saga Musix

Saga Musix

2016-03-03 01:29

administrator   ~0002271

Merged with some modifications in r6024.
By the way, since you have now submitted quite a few patches, I'd like to add you to the list of patch submitters in History.txt. Would you mind supplying your real name for that list?

Revenant

Revenant

2016-03-05 03:56

reporter   ~0002273

Sure, my real name is Devin Acker.

If you also want a web address there's http://revenant1.net (which is just a redirect to my github profile for now until I finish designing a new site proper).

Revenant

Revenant

2016-03-18 01:47

reporter   ~0002290

Last edited: 2016-03-18 05:27

One more patch...

This handles the "INFO" chunk more correctly, based on the format description here:
ftp://breakpoint.untergrund.net/users/buzz/exotica/development/format_descriptions/mod/PTFMT361.TXT

The document also describes an alternate module format based on pattern/instrument/sample chunks, but these were never actually implemented, so the loader ignores them (as well as INFO chunk fields which are not really useful for the regular format).

Saga Musix

Saga Musix

2016-03-18 19:04

administrator   ~0002296

Applied in r6143 with some slight tweaks.

Issue History

Date Modified Username Field Change
2016-02-27 02:56 Revenant New Issue
2016-02-27 02:56 Revenant File Added: pt36.patch
2016-02-27 02:56 Revenant File Added: Protracker 3.6.rar
2016-02-27 15:39 Saga Musix Assigned To => Saga Musix
2016-02-27 15:39 Saga Musix Status new => assigned
2016-03-03 01:29 Saga Musix Note Added: 0002271
2016-03-03 01:29 Saga Musix Status assigned => feedback
2016-03-03 01:30 Saga Musix Product Version => OpenMPT 1.25.04.00 / libopenmpt 0.2-beta16 (upgrade first)
2016-03-03 01:30 Saga Musix Fixed in Version => OpenMPT 1.26.01.00 / libopenmpt 0.2-beta17 (upgrade first)
2016-03-03 01:30 Saga Musix Target Version => OpenMPT 1.26.01.00 / libopenmpt 0.2-beta17 (upgrade first)
2016-03-05 03:56 Revenant Note Added: 0002273
2016-03-05 03:56 Revenant Status feedback => assigned
2016-03-05 10:19 Saga Musix Status assigned => resolved
2016-03-05 10:19 Saga Musix Resolution open => fixed
2016-03-18 01:47 Revenant Note Added: 0002290
2016-03-18 01:47 Revenant Status resolved => feedback
2016-03-18 01:47 Revenant Resolution fixed => reopened
2016-03-18 01:52 Saga Musix Status feedback => assigned
2016-03-18 05:26 Revenant File Added: pt36info.patch
2016-03-18 05:27 Revenant Note Edited: 0002290
2016-03-18 19:04 Saga Musix Note Added: 0002296
2016-03-18 19:04 Saga Musix Status assigned => resolved
2016-03-18 19:04 Saga Musix Resolution reopened => fixed