View Issue Details
| ID | Project | Category | View Status | Date Submitted | Last Update |
|---|---|---|---|---|---|
| 0000752 | OpenMPT | File Format Support | public | 2016-02-27 02:56 | 2016-03-18 19:04 |
| Reporter | Revenant | Assigned To | Saga Musix | ||
| Priority | normal | Severity | minor | Reproducibility | N/A |
| Status | resolved | Resolution | fixed | ||
| Product Version | OpenMPT 1.25.04.00 / libopenmpt 0.2-beta16 (upgrade first) | ||||
| Target Version | OpenMPT 1.26.01.00 / libopenmpt 0.2-beta17 (upgrade first) | Fixed in Version | OpenMPT 1.26.01.00 / libopenmpt 0.2-beta17 (upgrade first) | ||
| Summary | 0000752: 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) | ||||
| Tags | No 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" },
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);
| ||||
| Has the bug occurred in previous versions? | |||||
| Tested code revision (in case you know it) | |||||
|
Merged with some modifications in r6024. |
|
|
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). |
|
|
One more patch... This handles the "INFO" chunk more correctly, based on the format description here: 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). |
|
|
Applied in r6143 with some slight tweaks. |
|
| 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 |