/* * Load_it.cpp * ----------- * Purpose: IT (Impulse Tracker) module loader / saver * Notes : Also handles MPTM loading / saving, as the formats are almost identical. * Authors: Olivier Lapicque * OpenMPT Devs * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. */ #include "stdafx.h" #include "Loaders.h" #include "tuningcollection.h" #include "mod_specifications.h" #ifdef MODPLUG_TRACKER #include "../mptrack/Moddoc.h" #include "../mptrack/TrackerSettings.h" #endif // MODPLUG_TRACKER #ifdef MPT_EXTERNAL_SAMPLES #include "../common/mptPathString.h" #endif // MPT_EXTERNAL_SAMPLES #include "../common/serialization_utils.h" #ifndef MODPLUG_NO_FILESAVE #include "../common/mptFileIO.h" #endif // MODPLUG_NO_FILESAVE #include "plugins/PlugInterface.h" #include #include "../common/version.h" #include "ITTools.h" #include "mpt/io/base.hpp" #include "mpt/io/io.hpp" #include "mpt/io/io_stdstream.hpp" OPENMPT_NAMESPACE_BEGIN const uint16 verMptFileVer = 0x891; const uint16 verMptFileVerLoadLimit = 0x1000; // If cwtv-field is greater or equal to this value, // the MPTM file will not be loaded. /* MPTM version history for cwtv-field in "IT" header (only for MPTM files!): 0x890(1.18.02.00) -> 0x891(1.19.00.00): Pattern-specific time signatures Fixed behaviour of Pattern Loop command for rows > 255 (r617) 0x88F(1.18.01.00) -> 0x890(1.18.02.00): Removed volume command velocity :xy, added delay-cut command :xy. 0x88E(1.17.02.50) -> 0x88F(1.18.01.00): Numerous changes 0x88D(1.17.02.49) -> 0x88E(1.17.02.50): Changed ID to that of IT and undone the orderlist change done in 0x88A->0x88B. Now extended orderlist is saved as extension. 0x88C(1.17.02.48) -> 0x88D(1.17.02.49): Some tuning related changes - that part fails to read on older versions. 0x88B -> 0x88C: Changed type in which tuning number is printed to file: size_t -> uint16. 0x88A -> 0x88B: Changed order-to-pattern-index table type from uint8-array to vector. */ #ifndef MODPLUG_NO_FILESAVE static bool AreNonDefaultTuningsUsed(const CSoundFile& sf) { const INSTRUMENTINDEX numIns = sf.GetNumInstruments(); for(INSTRUMENTINDEX i = 1; i <= numIns; i++) { if(sf.Instruments[i] != nullptr && sf.Instruments[i]->pTuning != nullptr) return true; } return false; } static void WriteTuningCollection(std::ostream& oStrm, const CTuningCollection& tc) { tc.Serialize(oStrm, U_("Tune specific tunings")); } static void WriteTuningMap(std::ostream& oStrm, const CSoundFile& sf) { if(sf.GetNumInstruments() > 0) { //Writing instrument tuning data: first creating //tuning name <-> tuning id number map, //and then writing the tuning id for every instrument. //For example if there are 6 instruments and //first half use tuning 'T1', and the other half //tuning 'T2', the output would be something like //T1 1 T2 2 1 1 1 2 2 2 //Creating the tuning address <-> tuning id number map. std::map tNameToShort_Map; unsigned short figMap = 0; for(INSTRUMENTINDEX i = 1; i <= sf.GetNumInstruments(); i++) { CTuning *pTuning = nullptr; if(sf.Instruments[i] != nullptr) { pTuning = sf.Instruments[i]->pTuning; } auto iter = tNameToShort_Map.find(pTuning); if(iter != tNameToShort_Map.end()) continue; //Tuning already mapped. tNameToShort_Map[pTuning] = figMap; figMap++; } //...and write the map with tuning names replacing //the addresses. const uint16 tuningMapSize = static_cast(tNameToShort_Map.size()); mpt::IO::WriteIntLE(oStrm, tuningMapSize); for(auto &iter : tNameToShort_Map) { if(iter.first) mpt::IO::WriteSizedStringLE(oStrm, mpt::ToCharset(mpt::Charset::UTF8, iter.first->GetName())); else //Case: Using original IT tuning. mpt::IO::WriteSizedStringLE(oStrm, "->MPT_ORIGINAL_IT<-"); mpt::IO::WriteIntLE(oStrm, iter.second); } //Writing tuning data for instruments. for(INSTRUMENTINDEX i = 1; i <= sf.GetNumInstruments(); i++) { CTuning *pTuning = nullptr; if(sf.Instruments[i] != nullptr) { pTuning = sf.Instruments[i]->pTuning; } auto iter = tNameToShort_Map.find(pTuning); if(iter == tNameToShort_Map.end()) //Should never happen { sf.AddToLog(LogError, U_("Error: 210807_1")); return; } mpt::IO::WriteIntLE(oStrm, iter->second); } } } #endif // MODPLUG_NO_FILESAVE static void ReadTuningCollection(std::istream &iStrm, CTuningCollection &tc, const std::size_t dummy, mpt::Charset defaultCharset) { MPT_UNREFERENCED_PARAMETER(dummy); mpt::ustring name; tc.Deserialize(iStrm, name, defaultCharset); } template static bool ReadTuningMapTemplate(std::istream& iStrm, std::map &shortToTNameMap, mpt::Charset charset, const size_t maxNum = 500) { TUNNUMTYPE numTuning = 0; mpt::IO::ReadIntLE(iStrm, numTuning); if(numTuning > maxNum) return true; for(size_t i = 0; i < numTuning; i++) { std::string temp; uint16 ui = 0; if(!mpt::IO::ReadSizedStringLE(iStrm, temp, 255)) return true; mpt::IO::ReadIntLE(iStrm, ui); shortToTNameMap[ui] = mpt::ToUnicode(charset, temp); } if(iStrm.good()) return false; else return true; } static void ReadTuningMapImpl(std::istream& iStrm, CSoundFile& csf, mpt::Charset charset, const size_t = 0, bool old = false) { std::map shortToTNameMap; if(old) { ReadTuningMapTemplate(iStrm, shortToTNameMap, charset); } else { ReadTuningMapTemplate(iStrm, shortToTNameMap, charset); } // Read & set tunings for instruments std::vector notFoundTunings; for(INSTRUMENTINDEX i = 1; i<=csf.GetNumInstruments(); i++) { uint16 ui = 0; mpt::IO::ReadIntLE(iStrm, ui); auto iter = shortToTNameMap.find(ui); if(csf.Instruments[i] && iter != shortToTNameMap.end()) { const mpt::ustring str = iter->second; if(str == U_("->MPT_ORIGINAL_IT<-")) { csf.Instruments[i]->pTuning = nullptr; continue; } csf.Instruments[i]->pTuning = csf.GetTuneSpecificTunings().GetTuning(str); if(csf.Instruments[i]->pTuning) continue; #ifdef MODPLUG_TRACKER CTuning *localTuning = TrackerSettings::Instance().oldLocalTunings->GetTuning(str); if(localTuning) { std::unique_ptr pNewTuning = std::unique_ptr(new CTuning(*localTuning)); CTuning *pT = csf.GetTuneSpecificTunings().AddTuning(std::move(pNewTuning)); if(pT) { csf.AddToLog(LogInformation, U_("Local tunings are deprecated and no longer supported. Tuning '") + str + U_("' found in Local tunings has been copied to Tune-specific tunings and will be saved in the module file.")); csf.Instruments[i]->pTuning = pT; if(csf.GetpModDoc() != nullptr) { csf.GetpModDoc()->SetModified(); } continue; } else { csf.AddToLog(LogError, U_("Copying Local tuning '") + str + U_("' to Tune-specific tunings failed.")); } } #endif if(str == U_("12TET [[fs15 1.17.02.49]]") || str == U_("12TET")) { std::unique_ptr pNewTuning = csf.CreateTuning12TET(str); CTuning *pT = csf.GetTuneSpecificTunings().AddTuning(std::move(pNewTuning)); if(pT) { #ifdef MODPLUG_TRACKER csf.AddToLog(LogInformation, U_("Built-in tunings will no longer be used. Tuning '") + str + U_("' has been copied to Tune-specific tunings and will be saved in the module file.")); csf.Instruments[i]->pTuning = pT; if(csf.GetpModDoc() != nullptr) { csf.GetpModDoc()->SetModified(); } #endif continue; } else { #ifdef MODPLUG_TRACKER csf.AddToLog(LogError, U_("Copying Built-in tuning '") + str + U_("' to Tune-specific tunings failed.")); #endif } } // Checking if not found tuning already noticed. if(!mpt::contains(notFoundTunings, str)) { notFoundTunings.push_back(str); csf.AddToLog(LogWarning, U_("Tuning '") + str + U_("' used by the module was not found.")); #ifdef MODPLUG_TRACKER if(csf.GetpModDoc() != nullptr) { csf.GetpModDoc()->SetModified(); // The tuning is changed so the modified flag is set. } #endif // MODPLUG_TRACKER } csf.Instruments[i]->pTuning = csf.GetDefaultTuning(); } else { //This 'else' happens probably only in case of corrupted file. if(csf.Instruments[i]) csf.Instruments[i]->pTuning = csf.GetDefaultTuning(); } } //End read&set instrument tunings } static void ReadTuningMap(std::istream& iStrm, CSoundFile& csf, const size_t dummy, mpt::Charset charset) { ReadTuningMapImpl(iStrm, csf, charset, dummy, false); } ////////////////////////////////////////////////////////// // Impulse Tracker IT file support size_t CSoundFile::ITInstrToMPT(FileReader &file, ModInstrument &ins, uint16 trkvers) { if(trkvers < 0x0200) { // Load old format (IT 1.xx) instrument (early IT 2.xx modules may have cmwt set to 1.00 for backwards compatibility) ITOldInstrument instrumentHeader; if(!file.ReadStruct(instrumentHeader)) { return 0; } else { instrumentHeader.ConvertToMPT(ins); return sizeof(ITOldInstrument); } } else { const FileReader::off_t offset = file.GetPosition(); // Try loading extended instrument... instSize will differ between normal and extended instruments. ITInstrumentEx instrumentHeader; file.ReadStructPartial(instrumentHeader); size_t instSize = instrumentHeader.ConvertToMPT(ins, GetType()); file.Seek(offset + instSize); // Try reading modular instrument data. // Yes, it is completely idiotic that we have both this and LoadExtendedInstrumentProperties. // This is only required for files saved with *really* old OpenMPT versions (pre-1.17-RC1). // This chunk was also written in later versions (probably to maintain compatibility with // those ancient versions), but this also means that redundant information is stored in the file. // Starting from OpenMPT 1.25.02.07, this chunk is no longer written. if(file.ReadMagic("MSNI")) { //...the next piece of data must be the total size of the modular data FileReader modularData = file.ReadChunk(file.ReadUint32LE()); instSize += 8 + modularData.GetLength(); if(modularData.ReadMagic("GULP")) { ins.nMixPlug = modularData.ReadUint8(); if(ins.nMixPlug > MAX_MIXPLUGINS) ins.nMixPlug = 0; } } return instSize; } } static void CopyPatternName(CPattern &pattern, FileReader &file) { char name[MAX_PATTERNNAME] = ""; file.ReadString(name, MAX_PATTERNNAME); pattern.SetName(name); } // Get version of Schism Tracker that was used to create an IT/S3M file. mpt::ustring CSoundFile::GetSchismTrackerVersion(uint16 cwtv, uint32 reserved) { // Schism Tracker version information in a nutshell: // < 0x020: a proper version (files saved by such versions are likely very rare) // = 0x020: any version between the 0.2a release (2005-04-29?) and 2007-04-17 // = 0x050: anywhere from 2007-04-17 to 2009-10-31 // > 0x050: the number of days since 2009-10-31 // = 0xFFF: any version starting from 2020-10-28 (exact version stored in reserved value) cwtv &= 0xFFF; if(cwtv > 0x050) { int32 date = SchismTrackerEpoch + (cwtv < 0xFFF ? cwtv - 0x050 : reserved); int32 y = static_cast((Util::mul32to64(10000, date) + 14780) / 3652425); int32 ddd = date - (365 * y + y / 4 - y / 100 + y / 400); if(ddd < 0) { y--; ddd = date - (365 * y + y / 4 - y / 100 + y / 400); } int32 mi = (100 * ddd + 52) / 3060; return MPT_UFORMAT("Schism Tracker {}-{}-{}")( mpt::ufmt::dec0<4>(y + (mi + 2) / 12), mpt::ufmt::dec0<2>((mi + 2) % 12 + 1), mpt::ufmt::dec0<2>(ddd - (mi * 306 + 5) / 10 + 1)); } else { return MPT_UFORMAT("Schism Tracker 0.{}")(mpt::ufmt::hex0<2>(cwtv)); } } static bool ValidateHeader(const ITFileHeader &fileHeader) { if((std::memcmp(fileHeader.id, "IMPM", 4) && std::memcmp(fileHeader.id, "tpm.", 4)) || fileHeader.insnum > 0xFF || fileHeader.smpnum >= MAX_SAMPLES ) { return false; } return true; } static uint64 GetHeaderMinimumAdditionalSize(const ITFileHeader &fileHeader) { return fileHeader.ordnum + (fileHeader.insnum + fileHeader.smpnum + fileHeader.patnum) * 4; } CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderIT(MemoryFileReader file, const uint64 *pfilesize) { ITFileHeader fileHeader; if(!file.ReadStruct(fileHeader)) { return ProbeWantMoreData; } if(!ValidateHeader(fileHeader)) { return ProbeFailure; } return ProbeAdditionalSize(file, pfilesize, GetHeaderMinimumAdditionalSize(fileHeader)); } bool CSoundFile::ReadIT(FileReader &file, ModLoadingFlags loadFlags) { file.Rewind(); ITFileHeader fileHeader; if(!file.ReadStruct(fileHeader)) { return false; } if(!ValidateHeader(fileHeader)) { return false; } if(!file.CanRead(mpt::saturate_cast(GetHeaderMinimumAdditionalSize(fileHeader)))) { return false; } if(loadFlags == onlyVerifyHeader) { return true; } InitializeGlobals(MOD_TYPE_IT); bool interpretModPlugMade = false; mpt::ustring madeWithTracker; // OpenMPT crap at the end of file size_t mptStartPos = 0; if(!memcmp(fileHeader.id, "tpm.", 4)) { // Legacy MPTM files (old 1.17.02.4x releases) SetType(MOD_TYPE_MPT); file.Seek(file.GetLength() - 4); mptStartPos = file.ReadUint32LE(); } else { if(fileHeader.cwtv > 0x888 && fileHeader.cwtv <= 0xFFF) { file.Seek(file.GetLength() - 4); mptStartPos = file.ReadUint32LE(); if(mptStartPos >= 0x100 && mptStartPos < file.GetLength()) { if(file.Seek(mptStartPos) && file.ReadMagic("228")) { SetType(MOD_TYPE_MPT); if(fileHeader.cwtv >= verMptFileVerLoadLimit) { AddToLog(LogError, U_("The file informed that it is incompatible with this version of OpenMPT. Loading was terminated.")); return false; } else if(fileHeader.cwtv > verMptFileVer) { AddToLog(LogInformation, U_("The loaded file was made with a more recent OpenMPT version and this version may not be able to load all the features or play the file correctly.")); } } } } if(GetType() == MOD_TYPE_IT) { // Which tracker was used to make this? if((fileHeader.cwtv & 0xF000) == 0x5000) { // OpenMPT Version number (Major.Minor) // This will only be interpreted as "made with ModPlug" (i.e. disable compatible playback etc) if the "reserved" field is set to "OMPT" - else, compatibility was used. uint32 mptVersion = (fileHeader.cwtv & 0x0FFF) << 16; if(!memcmp(&fileHeader.reserved, "OMPT", 4)) interpretModPlugMade = true; else if(mptVersion >= 0x01'29'00'00) mptVersion |= fileHeader.reserved & 0xFFFF; m_dwLastSavedWithVersion = Version(mptVersion); } else if(fileHeader.cmwt == 0x888 || fileHeader.cwtv == 0x888) { // OpenMPT 1.17.02.26 (r122) to 1.18 (raped IT format) // Exact version number will be determined later. interpretModPlugMade = true; m_dwLastSavedWithVersion = MPT_V("1.17.00.00"); } else if(fileHeader.cwtv == 0x0217 && fileHeader.cmwt == 0x0200 && fileHeader.reserved == 0) { if(memchr(fileHeader.chnpan, 0xFF, sizeof(fileHeader.chnpan)) != nullptr) { // ModPlug Tracker 1.16 (semi-raped IT format) or BeRoTracker (will be determined later) m_dwLastSavedWithVersion = MPT_V("1.16.00.00"); madeWithTracker = U_("ModPlug Tracker 1.09 - 1.16"); } else { // OpenMPT 1.17 disguised as this in compatible mode, // but never writes 0xFF in the pan map for unused channels (which is an invalid value). m_dwLastSavedWithVersion = MPT_V("1.17.00.00"); madeWithTracker = U_("OpenMPT 1.17 (compatibility export)"); } interpretModPlugMade = true; } else if(fileHeader.cwtv == 0x0214 && fileHeader.cmwt == 0x0202 && fileHeader.reserved == 0) { // ModPlug Tracker b3.3 - 1.09, instruments 557 bytes apart m_dwLastSavedWithVersion = MPT_V("1.09.00.00"); madeWithTracker = U_("ModPlug Tracker b3.3 - 1.09"); interpretModPlugMade = true; } else if(fileHeader.cwtv == 0x0300 && fileHeader.cmwt == 0x0300 && fileHeader.reserved == 0 && fileHeader.ordnum == 256 && fileHeader.sep == 128 && fileHeader.pwd == 0) { // A rare variant used from OpenMPT 1.17.02.20 (r113) to 1.17.02.25 (r121), found e.g. in xTr1m-SD.it m_dwLastSavedWithVersion = MPT_V("1.17.02.20"); interpretModPlugMade = true; } } } m_SongFlags.set(SONG_LINEARSLIDES, (fileHeader.flags & ITFileHeader::linearSlides) != 0); m_SongFlags.set(SONG_ITOLDEFFECTS, (fileHeader.flags & ITFileHeader::itOldEffects) != 0); m_SongFlags.set(SONG_ITCOMPATGXX, (fileHeader.flags & ITFileHeader::itCompatGxx) != 0); m_SongFlags.set(SONG_EXFILTERRANGE, (fileHeader.flags & ITFileHeader::extendedFilterRange) != 0); m_songName = mpt::String::ReadBuf(mpt::String::spacePadded, fileHeader.songname); // Read row highlights if((fileHeader.special & ITFileHeader::embedPatternHighlights)) { // MPT 1.09 and older (and maybe also newer) versions leave this blank (0/0), but have the "special" flag set. // Newer versions of MPT and OpenMPT 1.17 *always* write 4/16 here. // Thus, we will just ignore those old versions. // Note: OpenMPT 1.17.03.02 was the first version to properly make use of the time signature in the IT header. // This poses a small unsolvable problem: // - In compatible mode, we cannot distinguish this version from earlier 1.17 releases. // Thus we cannot know when to read this field or not (m_dwLastSavedWithVersion will always be 1.17.00.00). // Luckily OpenMPT 1.17.03.02 should not be very wide-spread. // - In normal mode the time signature is always present in the song extensions anyway. So it's okay if we read // the signature here and maybe overwrite it later when parsing the song extensions. if(!m_dwLastSavedWithVersion || m_dwLastSavedWithVersion >= MPT_V("1.17.03.02")) { m_nDefaultRowsPerBeat = fileHeader.highlight_minor; m_nDefaultRowsPerMeasure = fileHeader.highlight_major; } } // Global Volume m_nDefaultGlobalVolume = fileHeader.globalvol << 1; if(m_nDefaultGlobalVolume > MAX_GLOBAL_VOLUME) m_nDefaultGlobalVolume = MAX_GLOBAL_VOLUME; if(fileHeader.speed) m_nDefaultSpeed = fileHeader.speed; m_nDefaultTempo.Set(std::max(uint8(31), static_cast(fileHeader.tempo))); m_nSamplePreAmp = std::min(static_cast(fileHeader.mv), uint8(128)); // Reading Channels Pan Positions for(CHANNELINDEX i = 0; i < 64; i++) if(fileHeader.chnpan[i] != 0xFF) { ChnSettings[i].Reset(); ChnSettings[i].nVolume = Clamp(fileHeader.chnvol[i], 0, 64); if(fileHeader.chnpan[i] & 0x80) ChnSettings[i].dwFlags.set(CHN_MUTE); uint8 n = fileHeader.chnpan[i] & 0x7F; if(n <= 64) ChnSettings[i].nPan = n * 4; if(n == 100) ChnSettings[i].dwFlags.set(CHN_SURROUND); } // Reading orders file.Seek(sizeof(ITFileHeader)); if(GetType() == MOD_TYPE_MPT && fileHeader.cwtv > 0x88A && fileHeader.cwtv <= 0x88D) { // Deprecated format used for MPTm files created with OpenMPT 1.17.02.46 - 1.17.02.48. uint16 version = file.ReadUint16LE(); if(version != 0) return false; uint32 numOrd = file.ReadUint32LE(); if(numOrd > ModSpecs::mptm.ordersMax || !ReadOrderFromFile(Order(), file, numOrd)) return false; } else { ReadOrderFromFile(Order(), file, fileHeader.ordnum, 0xFF, 0xFE); } // Reading instrument, sample and pattern offsets std::vector insPos, smpPos, patPos; if(!file.ReadVector(insPos, fileHeader.insnum) || !file.ReadVector(smpPos, fileHeader.smpnum) || !file.ReadVector(patPos, fileHeader.patnum)) { return false; } // Find the first parapointer. // This is used for finding out whether the edit history is actually stored in the file or not, // as some early versions of Schism Tracker set the history flag, but didn't save anything. // We will consider the history invalid if it ends after the first parapointer. uint32 minPtr = std::numeric_limits::max(); for(uint32 pos : insPos) { if(pos > 0 && pos < minPtr) minPtr = pos; } for(uint32 pos : smpPos) { if(pos > 0 && pos < minPtr) minPtr = pos; } for(uint32 pos : patPos) { if(pos > 0 && pos < minPtr) minPtr = pos; } if(fileHeader.special & ITFileHeader::embedSongMessage) { minPtr = std::min(minPtr, fileHeader.msgoffset.get()); } const bool possiblyUNMO3 = fileHeader.cmwt == 0x0214 && (fileHeader.cwtv == 0x0214 || fileHeader.cwtv == 0) && fileHeader.highlight_major == 0 && fileHeader.highlight_minor == 0 && fileHeader.pwd == 0 && fileHeader.reserved == 0 && (fileHeader.flags & (ITFileHeader::useMIDIPitchController | ITFileHeader::reqEmbeddedMIDIConfig)) == 0; if(possiblyUNMO3 && fileHeader.insnum == 0 && fileHeader.smpnum > 0 && file.GetPosition() + 4 * smpPos.size() + 2 <= minPtr) { // UNMO3 < v2.4.0.1 reserves some space for instrument parapointers even in sample mode. // This makes reading MIDI macros and plugin information impossible. // Note: While UNMO3 and CheeseTracker header fingerprints are almost identical, we cannot mis-detect CheeseTracker here, // as it always sets the instrument mode flag and writes non-zero row highlights. bool oldUNMO3 = true; for(uint16 i = 0; i < fileHeader.smpnum; i++) { if(file.ReadUint32LE() != 0) { oldUNMO3 = false; file.SkipBack(4 + i * 4); break; } } if(oldUNMO3) { madeWithTracker = U_("UNMO3 <= 2.4"); } } if(possiblyUNMO3 && fileHeader.cwtv == 0) { madeWithTracker = U_("UNMO3 v0/1"); } // Reading IT Edit History Info // This is only supposed to be present if bit 1 of the special flags is set. // However, old versions of Schism and probably other trackers always set this bit // even if they don't write the edit history count. So we have to filter this out... // This is done by looking at the parapointers. If the history data ends after // the first parapointer, we assume that it's actually no history data. if(fileHeader.special & ITFileHeader::embedEditHistory) { const uint16 nflt = file.ReadUint16LE(); if(file.CanRead(nflt * sizeof(ITHistoryStruct)) && file.GetPosition() + nflt * sizeof(ITHistoryStruct) <= minPtr) { m_FileHistory.resize(nflt); for(auto &mptHistory : m_FileHistory) { ITHistoryStruct itHistory; file.ReadStruct(itHistory); itHistory.ConvertToMPT(mptHistory); } if(possiblyUNMO3 && nflt == 0) { if(fileHeader.special & ITFileHeader::embedPatternHighlights) madeWithTracker = U_("UNMO3 <= 2.4.0.1"); // Set together with MIDI macro embed flag else madeWithTracker = U_("UNMO3"); // Either 2.4.0.2+ or no MIDI macros embedded } } else { // Oops, we were not supposed to read this. file.SkipBack(2); } } else if(possiblyUNMO3 && fileHeader.special <= 1) { // UNMO3 < v2.4.0.1 will set the edit history special bit iff the MIDI macro embed bit is also set, // but it always writes the two extra bytes for the edit history length (zeroes). // If MIDI macros are embedded, we are fine and end up in the first case of the if statement (read edit history). // Otherwise we end up here and might have to read the edit history length. if(file.ReadUint16LE() == 0) { madeWithTracker = U_("UNMO3 <= 2.4"); } else { // These were not zero bytes, but potentially belong to the upcoming MIDI config - need to skip back. // I think the only application that could end up here is CheeseTracker, if it allows to write 0 for both row highlight values. // IT 2.14 itself will always write the edit history. file.SkipBack(2); } } // Reading MIDI Output & Macros bool hasMidiConfig = (fileHeader.flags & ITFileHeader::reqEmbeddedMIDIConfig) || (fileHeader.special & ITFileHeader::embedMIDIConfiguration); if(hasMidiConfig && file.ReadStruct(m_MidiCfg)) { m_MidiCfg.Sanitize(); } // Ignore MIDI data. Fixes some files like denonde.it that were made with old versions of Impulse Tracker (which didn't support Zxx filters) and have Zxx effects in the patterns. if(fileHeader.cwtv < 0x0214) { m_MidiCfg.ClearZxxMacros(); } // Read pattern names: "PNAM" FileReader patNames; if(file.ReadMagic("PNAM")) { patNames = file.ReadChunk(file.ReadUint32LE()); } m_nChannels = 1; // Read channel names: "CNAM" if(file.ReadMagic("CNAM")) { FileReader chnNames = file.ReadChunk(file.ReadUint32LE()); const CHANNELINDEX readChns = std::min(MAX_BASECHANNELS, static_cast(chnNames.GetLength() / MAX_CHANNELNAME)); m_nChannels = readChns; for(CHANNELINDEX i = 0; i < readChns; i++) { chnNames.ReadString(ChnSettings[i].szName, MAX_CHANNELNAME); } } // Read mix plugins information FileReader pluginChunk = file.ReadChunk((minPtr >= file.GetPosition()) ? minPtr - file.GetPosition() : file.BytesLeft()); const bool isBeRoTracker = LoadMixPlugins(pluginChunk); // Read Song Message if((fileHeader.special & ITFileHeader::embedSongMessage) && fileHeader.msglength > 0 && file.Seek(fileHeader.msgoffset)) { // Generally, IT files should use CR for line endings. However, ChibiTracker uses LF. One could do... // if(itHeader.cwtv == 0x0214 && itHeader.cmwt == 0x0214 && itHeader.reserved == ITFileHeader::chibiMagic) --> Chibi detected. // But we'll just use autodetection here: m_songMessage.Read(file, fileHeader.msglength, SongMessage::leAutodetect); } // Reading Instruments m_nInstruments = 0; if(fileHeader.flags & ITFileHeader::instrumentMode) { m_nInstruments = std::min(static_cast(fileHeader.insnum), static_cast(MAX_INSTRUMENTS - 1)); } for(INSTRUMENTINDEX i = 0; i < GetNumInstruments(); i++) { if(insPos[i] > 0 && file.Seek(insPos[i]) && file.CanRead(fileHeader.cmwt < 0x200 ? sizeof(ITOldInstrument) : sizeof(ITInstrument))) { ModInstrument *instrument = AllocateInstrument(i + 1); if(instrument != nullptr) { ITInstrToMPT(file, *instrument, fileHeader.cmwt); // MIDI Pitch Wheel Depth is a global setting in IT. Apply it to all instruments. instrument->midiPWD = fileHeader.pwd; } } } // In order to properly compute the position, in file, of eventual extended settings // such as "attack" we need to keep the "real" size of the last sample as those extra // setting will follow this sample in the file FileReader::off_t lastSampleOffset = 0; if(fileHeader.smpnum > 0) { lastSampleOffset = smpPos[fileHeader.smpnum - 1] + sizeof(ITSample); } bool possibleXMconversion = false; // Reading Samples m_nSamples = std::min(static_cast(fileHeader.smpnum), static_cast(MAX_SAMPLES - 1)); bool lastSampleCompressed = false; for(SAMPLEINDEX i = 0; i < GetNumSamples(); i++) { ITSample sampleHeader; if(smpPos[i] > 0 && file.Seek(smpPos[i]) && file.ReadStruct(sampleHeader)) { // IT does not check for the IMPS magic, and some bad XM->IT converter out there doesn't write the magic bytes for empty sample slots. ModSample &sample = Samples[i + 1]; size_t sampleOffset = sampleHeader.ConvertToMPT(sample); m_szNames[i + 1] = mpt::String::ReadBuf(mpt::String::spacePadded, sampleHeader.name); if(!file.Seek(sampleOffset)) continue; lastSampleCompressed = false; if(sample.uFlags[CHN_ADLIB]) { // FM instrument in MPTM OPLPatch patch; if(file.ReadArray(patch)) { sample.SetAdlib(true, patch); } } else if(!sample.uFlags[SMP_KEEPONDISK]) { SampleIO sampleIO = sampleHeader.GetSampleFormat(fileHeader.cwtv); if(loadFlags & loadSampleData) { sampleIO.ReadSample(sample, file); } else { if(sampleIO.IsVariableLengthEncoded()) lastSampleCompressed = true; else file.Skip(sampleIO.CalculateEncodedSize(sample.nLength)); } if(sampleIO.GetEncoding() == SampleIO::unsignedPCM && sample.nLength != 0) { // There is some XM to IT converter (don't know which one) and it identifies as IT 2.04. // The only safe way to distinguish it from an IT-saved file are the unsigned samples. possibleXMconversion = true; } } else { // External sample in MPTM file size_t strLen; file.ReadVarInt(strLen); if((loadFlags & loadSampleData) && strLen) { std::string filenameU8; file.ReadString(filenameU8, strLen); #if defined(MPT_EXTERNAL_SAMPLES) SetSamplePath(i + 1, mpt::PathString::FromUTF8(filenameU8)); #elif !defined(LIBOPENMPT_BUILD_TEST) AddToLog(LogWarning, MPT_UFORMAT("Loading external sample {} ('{}') failed: External samples are not supported.")(i + 1, mpt::ToUnicode(mpt::Charset::UTF8, filenameU8))); #endif // MPT_EXTERNAL_SAMPLES } else { file.Skip(strLen); } } lastSampleOffset = std::max(lastSampleOffset, file.GetPosition()); } } m_nSamples = std::max(SAMPLEINDEX(1), GetNumSamples()); if(possibleXMconversion && fileHeader.cwtv == 0x0204 && fileHeader.cmwt == 0x0200 && fileHeader.special == 0 && fileHeader.reserved == 0 && (fileHeader.flags & ~ITFileHeader::linearSlides) == (ITFileHeader::useStereoPlayback | ITFileHeader::instrumentMode | ITFileHeader::itOldEffects) && fileHeader.globalvol == 128 && fileHeader.mv == 48 && fileHeader.sep == 128 && fileHeader.pwd == 0 && fileHeader.msglength == 0) { for(uint8 pan : fileHeader.chnpan) { if(pan != 0x20 && pan != 0xA0) possibleXMconversion = false; } for(uint8 vol : fileHeader.chnvol) { if(vol != 0x40) possibleXMconversion = false; } for(size_t i = 20; i < std::size(fileHeader.songname); i++) { if(fileHeader.songname[i] != 0) possibleXMconversion = false; } if(possibleXMconversion) madeWithTracker = U_("XM Conversion"); } m_nMinPeriod = 0; m_nMaxPeriod = int32_max; PATTERNINDEX numPats = std::min(static_cast(patPos.size()), GetModSpecifications().patternsMax); if(numPats != patPos.size()) { // Hack: Notify user here if file contains more patterns than what can be read. AddToLog(LogWarning, MPT_UFORMAT("The module contains {} patterns but only {} patterns can be loaded in this OpenMPT version.")(patPos.size(), numPats)); } if(!(loadFlags & loadPatternData)) { numPats = 0; } // Checking for number of used channels, which is not explicitely specified in the file. for(PATTERNINDEX pat = 0; pat < numPats; pat++) { if(patPos[pat] == 0 || !file.Seek(patPos[pat])) continue; uint16 len = file.ReadUint16LE(); ROWINDEX numRows = file.ReadUint16LE(); if(numRows < 1 || numRows > MAX_PATTERN_ROWS || !file.Skip(4)) continue; FileReader patternData = file.ReadChunk(len); ROWINDEX row = 0; std::vector chnMask(GetNumChannels()); while(row < numRows && patternData.CanRead(1)) { uint8 b = patternData.ReadUint8(); if(!b) { row++; continue; } CHANNELINDEX ch = (b & IT_bitmask_patternChanField_c); // 0x7f We have some data grab a byte keeping only 7 bits if(ch) { ch = (ch - 1);// & IT_bitmask_patternChanMask_c; // 0x3f mask of the byte again, keeping only 6 bits } if(ch >= chnMask.size()) { chnMask.resize(ch + 1, 0); } if(b & IT_bitmask_patternChanEnabled_c) // 0x80 check if the upper bit is enabled. { chnMask[ch] = patternData.ReadUint8(); // set the channel mask for this channel. } // Channel used if(chnMask[ch] & 0x0F) // if this channel is used set m_nChannels { if(ch >= GetNumChannels() && ch < MAX_BASECHANNELS) { m_nChannels = ch + 1; } } // Now we actually update the pattern-row entry the note,instrument etc. // Note if(chnMask[ch] & 1) patternData.Skip(1); // Instrument if(chnMask[ch] & 2) patternData.Skip(1); // Volume if(chnMask[ch] & 4) patternData.Skip(1); // Effect if(chnMask[ch] & 8) patternData.Skip(2); } lastSampleOffset = std::max(lastSampleOffset, file.GetPosition()); } // Compute extra instruments settings position if(lastSampleOffset > 0) { file.Seek(lastSampleOffset); if(lastSampleCompressed) { // If the last sample was compressed, we do not know where it ends. // Hence, in case we decided not to decode the sample data, we now // have to emulate this until we reach EOF or some instrument / song properties. while(file.CanRead(4)) { if(file.ReadMagic("XTPM") || file.ReadMagic("STPM")) { uint32 id = file.ReadUint32LE(); file.SkipBack(8); // Our chunk IDs should only contain ASCII characters if(!(id & 0x80808080) && (id & 0x60606060)) { break; } } file.Skip(file.ReadUint16LE()); } } } // Load instrument and song extensions. interpretModPlugMade |= LoadExtendedInstrumentProperties(file); if(interpretModPlugMade && !isBeRoTracker) { m_playBehaviour.reset(); m_nMixLevels = MixLevels::Original; } // Need to do this before reading the patterns because m_nChannels might be modified by LoadExtendedSongProperties. *sigh* LoadExtendedSongProperties(file, false, &interpretModPlugMade); // Reading Patterns Patterns.ResizeArray(numPats); for(PATTERNINDEX pat = 0; pat < numPats; pat++) { if(patPos[pat] == 0 || !file.Seek(patPos[pat])) { // Empty 64-row pattern if(!Patterns.Insert(pat, 64)) { AddToLog(LogWarning, MPT_UFORMAT("Allocating patterns failed starting from pattern {}")(pat)); break; } // Now (after the Insert() call), we can read the pattern name. CopyPatternName(Patterns[pat], patNames); continue; } uint16 len = file.ReadUint16LE(); ROWINDEX numRows = file.ReadUint16LE(); if(!file.Skip(4) || !Patterns.Insert(pat, numRows)) continue; FileReader patternData = file.ReadChunk(len); // Now (after the Insert() call), we can read the pattern name. CopyPatternName(Patterns[pat], patNames); std::vector chnMask(GetNumChannels()); std::vector lastValue(GetNumChannels(), ModCommand::Empty()); auto patData = Patterns[pat].begin(); ROWINDEX row = 0; while(row < numRows && patternData.CanRead(1)) { uint8 b = patternData.ReadUint8(); if(!b) { row++; patData += GetNumChannels(); continue; } CHANNELINDEX ch = b & IT_bitmask_patternChanField_c; // 0x7f if(ch) { ch = (ch - 1); //& IT_bitmask_patternChanMask_c; // 0x3f } if(ch >= chnMask.size()) { chnMask.resize(ch + 1, 0); lastValue.resize(ch + 1, ModCommand::Empty()); MPT_ASSERT(chnMask.size() <= GetNumChannels()); } if(b & IT_bitmask_patternChanEnabled_c) // 0x80 { chnMask[ch] = patternData.ReadUint8(); } // Now we grab the data for this particular row/channel. ModCommand dummy = ModCommand::Empty(); ModCommand &m = ch < m_nChannels ? patData[ch] : dummy; if(chnMask[ch] & 0x10) { m.note = lastValue[ch].note; } if(chnMask[ch] & 0x20) { m.instr = lastValue[ch].instr; } if(chnMask[ch] & 0x40) { m.volcmd = lastValue[ch].volcmd; m.vol = lastValue[ch].vol; } if(chnMask[ch] & 0x80) { m.command = lastValue[ch].command; m.param = lastValue[ch].param; } if(chnMask[ch] & 1) // Note { uint8 note = patternData.ReadUint8(); if(note < 0x80) note += NOTE_MIN; if(!(GetType() & MOD_TYPE_MPT)) { if(note > NOTE_MAX && note < 0xFD) note = NOTE_FADE; else if(note == 0xFD) note = NOTE_NONE; } m.note = note; lastValue[ch].note = note; } if(chnMask[ch] & 2) { uint8 instr = patternData.ReadUint8(); m.instr = instr; lastValue[ch].instr = instr; } if(chnMask[ch] & 4) { uint8 vol = patternData.ReadUint8(); // 0-64: Set Volume if(vol <= 64) { m.volcmd = VOLCMD_VOLUME; m.vol = vol; } else // 128-192: Set Panning if(vol >= 128 && vol <= 192) { m.volcmd = VOLCMD_PANNING; m.vol = vol - 128; } else // 65-74: Fine Volume Up if(vol < 75) { m.volcmd = VOLCMD_FINEVOLUP; m.vol = vol - 65; } else // 75-84: Fine Volume Down if(vol < 85) { m.volcmd = VOLCMD_FINEVOLDOWN; m.vol = vol - 75; } else // 85-94: Volume Slide Up if(vol < 95) { m.volcmd = VOLCMD_VOLSLIDEUP; m.vol = vol - 85; } else // 95-104: Volume Slide Down if(vol < 105) { m.volcmd = VOLCMD_VOLSLIDEDOWN; m.vol = vol - 95; } else // 105-114: Pitch Slide Up if(vol < 115) { m.volcmd = VOLCMD_PORTADOWN; m.vol = vol - 105; } else // 115-124: Pitch Slide Down if(vol < 125) { m.volcmd = VOLCMD_PORTAUP; m.vol = vol - 115; } else // 193-202: Portamento To if(vol >= 193 && vol <= 202) { m.volcmd = VOLCMD_TONEPORTAMENTO; m.vol = vol - 193; } else // 203-212: Vibrato depth if(vol >= 203 && vol <= 212) { m.volcmd = VOLCMD_VIBRATODEPTH; m.vol = vol - 203; // Old versions of ModPlug saved this as vibrato speed instead, so let's fix that. if(m.vol && m_dwLastSavedWithVersion && m_dwLastSavedWithVersion <= MPT_V("1.17.02.54")) m.volcmd = VOLCMD_VIBRATOSPEED; } else // 213-222: Unused (was velocity) // 223-232: Offset if(vol >= 223 && vol <= 232) { m.volcmd = VOLCMD_OFFSET; m.vol = vol - 223; } lastValue[ch].volcmd = m.volcmd; lastValue[ch].vol = m.vol; } // Reading command/param if(chnMask[ch] & 8) { const auto [command, param] = patternData.ReadArray(); m.command = command; m.param = param; S3MConvert(m, true); // In some IT-compatible trackers, it is possible to input a parameter without a command. // In this case, we still need to update the last value memory. OpenMPT didn't do this until v1.25.01.07. // Example: ckbounce.it lastValue[ch].command = m.command; lastValue[ch].param = m.param; } } } if(!m_dwLastSavedWithVersion && fileHeader.cwtv == 0x0888) { // Up to OpenMPT 1.17.02.45 (r165), it was possible that the "last saved with" field was 0 // when saving a file in OpenMPT for the first time. m_dwLastSavedWithVersion = MPT_V("1.17.00.00"); } if(m_dwLastSavedWithVersion && madeWithTracker.empty()) { madeWithTracker = U_("OpenMPT ") + mpt::ufmt::val(m_dwLastSavedWithVersion); if(memcmp(&fileHeader.reserved, "OMPT", 4) && (fileHeader.cwtv & 0xF000) == 0x5000) { madeWithTracker += U_(" (compatibility export)"); } else if(m_dwLastSavedWithVersion.IsTestVersion()) { madeWithTracker += U_(" (test build)"); } } else { const int32 schismDateVersion = SchismTrackerEpoch + ((fileHeader.cwtv == 0x1FFF) ? fileHeader.reserved : (fileHeader.cwtv - 0x1050)); switch(fileHeader.cwtv >> 12) { case 0: if(isBeRoTracker) { // Old versions madeWithTracker = U_("BeRoTracker"); } else if(fileHeader.cwtv == 0x0214 && fileHeader.cmwt == 0x0200 && fileHeader.flags == 9 && fileHeader.special == 0 && fileHeader.highlight_major == 0 && fileHeader.highlight_minor == 0 && fileHeader.insnum == 0 && fileHeader.patnum + 1 == fileHeader.ordnum && fileHeader.globalvol == 128 && fileHeader.mv == 100 && fileHeader.speed == 1 && fileHeader.sep == 128 && fileHeader.pwd == 0 && fileHeader.msglength == 0 && fileHeader.msgoffset == 0 && fileHeader.reserved == 0) { madeWithTracker = U_("OpenSPC conversion"); } else if(fileHeader.cwtv == 0x0214 && fileHeader.cmwt == 0x0200 && fileHeader.highlight_major == 0 && fileHeader.highlight_minor == 0 && fileHeader.reserved == 0) { // ModPlug Tracker 1.00a5, instruments 560 bytes apart m_dwLastSavedWithVersion = MPT_V("1.00.00.A5"); madeWithTracker = U_("ModPlug Tracker 1.00a5"); interpretModPlugMade = true; } else if(fileHeader.cwtv == 0x0214 && fileHeader.cmwt == 0x0214 && !memcmp(&fileHeader.reserved, "CHBI", 4)) { madeWithTracker = U_("ChibiTracker"); m_playBehaviour.reset(kITShortSampleRetrig); } else if(fileHeader.cwtv == 0x0214 && fileHeader.cmwt == 0x0214 && fileHeader.special <= 1 && fileHeader.pwd == 0 && fileHeader.reserved == 0 && (fileHeader.flags & (ITFileHeader::vol0Optimisations | ITFileHeader::instrumentMode | ITFileHeader::useMIDIPitchController | ITFileHeader::reqEmbeddedMIDIConfig | ITFileHeader::extendedFilterRange)) == ITFileHeader::instrumentMode && m_nSamples > 0 && (Samples[1].filename == "XXXXXXXX.YYY")) { madeWithTracker = U_("CheeseTracker"); } else if(fileHeader.cwtv == 0 && madeWithTracker.empty()) { madeWithTracker = U_("Unknown"); } else if(fileHeader.cmwt < 0x0300 && madeWithTracker.empty()) { if(fileHeader.cmwt > 0x0214) { madeWithTracker = U_("Impulse Tracker 2.15"); } else if(fileHeader.cwtv > 0x0214) { // Patched update of IT 2.14 (0x0215 - 0x0217 == p1 - p3) // p4 (as found on modland) adds the ITVSOUND driver, but doesn't seem to change // anything as far as file saving is concerned. madeWithTracker = MPT_UFORMAT("Impulse Tracker 2.14p{}")(fileHeader.cwtv - 0x0214); } else { madeWithTracker = MPT_UFORMAT("Impulse Tracker {}.{}")((fileHeader.cwtv & 0x0F00) >> 8, mpt::ufmt::hex0<2>((fileHeader.cwtv & 0xFF))); } if(m_FileHistory.empty() && fileHeader.reserved != 0) { // Starting from version 2.07, IT stores the total edit time of a module in the "reserved" field uint32 editTime = DecodeITEditTimer(fileHeader.cwtv, fileHeader.reserved); FileHistory hist; hist.openTime = static_cast(editTime * (HISTORY_TIMER_PRECISION / 18.2)); m_FileHistory.push_back(hist); } } break; case 1: madeWithTracker = GetSchismTrackerVersion(fileHeader.cwtv, fileHeader.reserved); // Hertz in linear mode: Added 2015-01-29, https://github.com/schismtracker/schismtracker/commit/671b30311082a0e7df041fca25f989b5d2478f69 if(schismDateVersion < SchismVersionFromDate<2015, 01, 29>::date && m_SongFlags[SONG_LINEARSLIDES]) m_playBehaviour.reset(kPeriodsAreHertz); // Hertz in Amiga mode: Added 2021-05-02, https://github.com/schismtracker/schismtracker/commit/c656a6cbd5aaf81198a7580faf81cb7960cb6afa else if(schismDateVersion < SchismVersionFromDate<2021, 05, 02>::date && !m_SongFlags[SONG_LINEARSLIDES]) m_playBehaviour.reset(kPeriodsAreHertz); // Qxx with short samples: Added 2016-05-13, https://github.com/schismtracker/schismtracker/commit/e7b1461fe751554309fd403713c2a1ef322105ca if(schismDateVersion < SchismVersionFromDate<2016, 05, 13>::date) m_playBehaviour.reset(kITShortSampleRetrig); // Instrument pan doesn't override channel pan: Added 2021-05-02, https://github.com/schismtracker/schismtracker/commit/a34ec86dc819915debc9e06f4727b77bf2dd29ee if(schismDateVersion < SchismVersionFromDate<2021, 05, 02>::date) m_playBehaviour.reset(kITDoNotOverrideChannelPan); // Notes set instrument panning, not instrument numbers: Added 2021-05-02, https://github.com/schismtracker/schismtracker/commit/648f5116f984815c69e11d018b32dfec53c6b97a if(schismDateVersion < SchismVersionFromDate<2021, 05, 02>::date) m_playBehaviour.reset(kITPanningReset); // Imprecise calculation of ping-pong loop wraparound: Added 2021-11-01, https://github.com/schismtracker/schismtracker/commit/22cbb9b676e9c2c9feb7a6a17deca7a17ac138cc if(schismDateVersion < SchismVersionFromDate<2021, 11, 01>::date) m_playBehaviour.set(kImprecisePingPongLoops); // Pitch/Pan Separation can be overridden by panning commands: Added 2021-11-01, https://github.com/schismtracker/schismtracker/commit/6e9f1207015cae0fe1b829fff7bb867e02ec6dea if(schismDateVersion < SchismVersionFromDate<2021, 11, 01>::date) m_playBehaviour.reset(kITPitchPanSeparation); break; case 4: madeWithTracker = MPT_UFORMAT("pyIT {}.{}")((fileHeader.cwtv & 0x0F00) >> 8, mpt::ufmt::hex0<2>(fileHeader.cwtv & 0xFF)); break; case 6: madeWithTracker = U_("BeRoTracker"); break; case 7: if(fileHeader.cwtv == 0x7FFF && fileHeader.cmwt == 0x0215) madeWithTracker = U_("munch.py"); else madeWithTracker = MPT_UFORMAT("ITMCK {}.{}.{}")((fileHeader.cwtv >> 8) & 0x0F, (fileHeader.cwtv >> 4) & 0x0F, fileHeader.cwtv & 0x0F); break; case 0xD: madeWithTracker = U_("spc2it"); break; } } if(GetType() == MOD_TYPE_MPT) { // START - mpt specific: if(fileHeader.cwtv > 0x0889 && file.Seek(mptStartPos)) { LoadMPTMProperties(file, fileHeader.cwtv); } } m_modFormat.formatName = (GetType() == MOD_TYPE_MPT) ? U_("OpenMPT MPTM") : MPT_UFORMAT("Impulse Tracker {}.{}")(fileHeader.cmwt >> 8, mpt::ufmt::hex0<2>(fileHeader.cmwt & 0xFF)); m_modFormat.type = (GetType() == MOD_TYPE_MPT) ? U_("mptm") : U_("it"); m_modFormat.madeWithTracker = std::move(madeWithTracker); m_modFormat.charset = m_dwLastSavedWithVersion ? mpt::Charset::Windows1252 : mpt::Charset::CP437; return true; } void CSoundFile::LoadMPTMProperties(FileReader &file, uint16 cwtv) { std::istringstream iStrm(mpt::buffer_cast(file.GetRawDataAsByteVector())); if(cwtv >= 0x88D) { srlztn::SsbRead ssb(iStrm); ssb.BeginRead("mptm", Version::Current().GetRawVersion()); int8 useUTF8Tuning = 0; ssb.ReadItem(useUTF8Tuning, "UTF8Tuning"); mpt::Charset TuningCharset = useUTF8Tuning ? mpt::Charset::UTF8 : GetCharsetInternal(); ssb.ReadItem(GetTuneSpecificTunings(), "0", [TuningCharset](std::istream &iStrm, CTuningCollection &tc, const std::size_t dummy){ return ReadTuningCollection(iStrm, tc, dummy, TuningCharset); }); ssb.ReadItem(*this, "1", [TuningCharset](std::istream& iStrm, CSoundFile& csf, const std::size_t dummy){ return ReadTuningMap(iStrm, csf, dummy, TuningCharset); }); ssb.ReadItem(Order, "2", &ReadModSequenceOld); ssb.ReadItem(Patterns, FileIdPatterns, &ReadModPatterns); mpt::Charset sequenceDefaultCharset = GetCharsetInternal(); ssb.ReadItem(Order, FileIdSequences, [sequenceDefaultCharset](std::istream &iStrm, ModSequenceSet &seq, std::size_t nSize){ return ReadModSequences(iStrm, seq, nSize, sequenceDefaultCharset); }); if(ssb.GetStatus() & srlztn::SNT_FAILURE) { AddToLog(LogError, U_("Unknown error occurred while deserializing file.")); } } else { // Loading for older files. mpt::ustring name; if(GetTuneSpecificTunings().Deserialize(iStrm, name, GetCharsetInternal()) != Tuning::SerializationResult::Success) { AddToLog(LogError, U_("Loading tune specific tunings failed.")); } else { ReadTuningMapImpl(iStrm, *this, GetCharsetInternal(), 0, cwtv < 0x88C); } } } #ifndef MODPLUG_NO_FILESAVE // Save edit history. Pass a null pointer for *f to retrieve the number of bytes that would be written. static uint32 SaveITEditHistory(const CSoundFile &sndFile, std::ostream *file) { size_t num = sndFile.GetFileHistory().size(); #ifdef MODPLUG_TRACKER const CModDoc *pModDoc = sndFile.GetpModDoc(); num += (pModDoc != nullptr) ? 1 : 0; // + 1 for this session #endif // MODPLUG_TRACKER uint16 fnum = mpt::saturate_cast(num); // Number of entries that are actually going to be written const uint32 bytesWritten = 2 + fnum * 8; // Number of bytes that are actually going to be written if(!file) { return bytesWritten; } std::ostream & f = *file; // Write number of history entries mpt::IO::WriteIntLE(f, fnum); // Write history data const size_t start = (num > uint16_max) ? num - uint16_max : 0; for(size_t n = start; n < num; n++) { FileHistory mptHistory; #ifdef MODPLUG_TRACKER if(n < sndFile.GetFileHistory().size()) #endif // MODPLUG_TRACKER { // Previous timestamps mptHistory = sndFile.GetFileHistory()[n]; #ifdef MODPLUG_TRACKER } else if(pModDoc != nullptr) { // Current ("new") timestamp const time_t creationTime = pModDoc->GetCreationTime(); MemsetZero(mptHistory.loadDate); //localtime_s(&loadDate, &creationTime); const tm* const p = localtime(&creationTime); if (p != nullptr) mptHistory.loadDate = *p; else sndFile.AddToLog(LogError, U_("Unable to retrieve current time.")); mptHistory.openTime = (uint32)(difftime(time(nullptr), creationTime) * HISTORY_TIMER_PRECISION); #endif // MODPLUG_TRACKER } ITHistoryStruct itHistory; itHistory.ConvertToIT(mptHistory); mpt::IO::Write(f, itHistory); } return bytesWritten; } bool CSoundFile::SaveIT(std::ostream &f, const mpt::PathString &filename, bool compatibilityExport) { const CModSpecifications &specs = (GetType() == MOD_TYPE_MPT ? ModSpecs::mptm : (compatibilityExport ? ModSpecs::it : ModSpecs::itEx)); uint32 dwChnNamLen; ITFileHeader itHeader; uint64 dwPos = 0; uint32 dwHdrPos = 0, dwExtra = 0; // Writing Header MemsetZero(itHeader); dwChnNamLen = 0; memcpy(itHeader.id, "IMPM", 4); mpt::String::WriteBuf(mpt::String::nullTerminated, itHeader.songname) = m_songName; itHeader.highlight_minor = mpt::saturate_cast(m_nDefaultRowsPerBeat); itHeader.highlight_major = mpt::saturate_cast(m_nDefaultRowsPerMeasure); if(GetType() == MOD_TYPE_MPT) { itHeader.ordnum = Order().GetLengthTailTrimmed(); if(Order().NeedsExtraDatafield() && itHeader.ordnum > 256) { // If there are more order items, write them elsewhere. itHeader.ordnum = 256; } } else { // An additional "---" pattern is appended so Impulse Tracker won't ignore the last order item. // Interestingly, this can exceed IT's 256 order limit. Also, IT will always save at least two orders. itHeader.ordnum = std::min(Order().GetLengthTailTrimmed(), specs.ordersMax) + 1; if(itHeader.ordnum < 2) itHeader.ordnum = 2; } itHeader.insnum = std::min(m_nInstruments, specs.instrumentsMax); itHeader.smpnum = std::min(m_nSamples, specs.samplesMax); itHeader.patnum = std::min(Patterns.GetNumPatterns(), specs.patternsMax); // Parapointers std::vector patpos(itHeader.patnum); std::vector smppos(itHeader.smpnum); std::vector inspos(itHeader.insnum); //VERSION if(GetType() == MOD_TYPE_MPT) { // MPTM itHeader.cwtv = verMptFileVer; // Used in OMPT-hack versioning. itHeader.cmwt = 0x888; } else { // IT const uint32 mptVersion = Version::Current().GetRawVersion(); itHeader.cwtv = 0x5000 | static_cast((mptVersion >> 16) & 0x0FFF); // format: txyy (t = tracker ID, x = version major, yy = version minor), e.g. 0x5117 (OpenMPT = 5, 117 = v1.17) itHeader.cmwt = 0x0214; // Common compatible tracker :) // Hack from schism tracker: for(INSTRUMENTINDEX nIns = 1; nIns <= GetNumInstruments(); nIns++) { if(Instruments[nIns] && Instruments[nIns]->PitchEnv.dwFlags[ENV_FILTER]) { itHeader.cmwt = 0x0216; break; } } if(compatibilityExport) itHeader.reserved = mptVersion & 0xFFFF; else memcpy(&itHeader.reserved, "OMPT", 4); } itHeader.flags = ITFileHeader::useStereoPlayback | ITFileHeader::useMIDIPitchController; itHeader.special = ITFileHeader::embedEditHistory | ITFileHeader::embedPatternHighlights; if(m_nInstruments) itHeader.flags |= ITFileHeader::instrumentMode; if(m_SongFlags[SONG_LINEARSLIDES]) itHeader.flags |= ITFileHeader::linearSlides; if(m_SongFlags[SONG_ITOLDEFFECTS]) itHeader.flags |= ITFileHeader::itOldEffects; if(m_SongFlags[SONG_ITCOMPATGXX]) itHeader.flags |= ITFileHeader::itCompatGxx; if(m_SongFlags[SONG_EXFILTERRANGE] && !compatibilityExport) itHeader.flags |= ITFileHeader::extendedFilterRange; itHeader.globalvol = static_cast(m_nDefaultGlobalVolume / 2u); itHeader.mv = static_cast(std::min(m_nSamplePreAmp, uint32(128))); itHeader.speed = mpt::saturate_cast(m_nDefaultSpeed); itHeader.tempo = mpt::saturate_cast(m_nDefaultTempo.GetInt()); // We save the real tempo in an extension below if it exceeds 255. itHeader.sep = 128; // pan separation // IT doesn't have a per-instrument Pitch Wheel Depth setting, so we just store the first non-zero PWD setting in the header. for(INSTRUMENTINDEX ins = 1; ins <= GetNumInstruments(); ins++) { if(Instruments[ins] != nullptr && Instruments[ins]->midiPWD != 0) { itHeader.pwd = static_cast(std::abs(Instruments[ins]->midiPWD)); break; } } dwHdrPos = sizeof(itHeader) + itHeader.ordnum; // Channel Pan and Volume memset(itHeader.chnpan, 0xA0, 64); memset(itHeader.chnvol, 64, 64); for(CHANNELINDEX ich = 0; ich < std::min(m_nChannels, CHANNELINDEX(64)); ich++) // Header only has room for settings for 64 chans... { itHeader.chnpan[ich] = (uint8)(ChnSettings[ich].nPan >> 2); if (ChnSettings[ich].dwFlags[CHN_SURROUND]) itHeader.chnpan[ich] = 100; itHeader.chnvol[ich] = (uint8)(ChnSettings[ich].nVolume); #ifdef MODPLUG_TRACKER if(TrackerSettings::Instance().MiscSaveChannelMuteStatus) #endif if (ChnSettings[ich].dwFlags[CHN_MUTE]) itHeader.chnpan[ich] |= 0x80; } // Channel names if(!compatibilityExport) { for(CHANNELINDEX i = 0; i < m_nChannels; i++) { if(ChnSettings[i].szName[0]) { dwChnNamLen = (i + 1) * MAX_CHANNELNAME; } } if(dwChnNamLen) dwExtra += dwChnNamLen + 8; } if(!m_MidiCfg.IsMacroDefaultSetupUsed()) { itHeader.flags |= ITFileHeader::reqEmbeddedMIDIConfig; itHeader.special |= ITFileHeader::embedMIDIConfiguration; dwExtra += sizeof(MIDIMacroConfigData); } // Pattern Names const PATTERNINDEX numNamedPats = compatibilityExport ? 0 : Patterns.GetNumNamedPatterns(); if(numNamedPats > 0) { dwExtra += (numNamedPats * MAX_PATTERNNAME) + 8; } // Mix Plugins. Just calculate the size of this extra block for now. if(!compatibilityExport) { dwExtra += SaveMixPlugins(nullptr, true); } // Edit History. Just calculate the size of this extra block for now. dwExtra += SaveITEditHistory(*this, nullptr); // Comments uint16 msglength = 0; if(!m_songMessage.empty()) { itHeader.special |= ITFileHeader::embedSongMessage; itHeader.msglength = msglength = mpt::saturate_cast(m_songMessage.length() + 1u); itHeader.msgoffset = dwHdrPos + dwExtra + (itHeader.insnum + itHeader.smpnum + itHeader.patnum) * 4; } // Write file header mpt::IO::Write(f, itHeader); Order().WriteAsByte(f, itHeader.ordnum); mpt::IO::Write(f, inspos); mpt::IO::Write(f, smppos); mpt::IO::Write(f, patpos); // Writing edit history information SaveITEditHistory(*this, &f); // Writing midi cfg if(itHeader.flags & ITFileHeader::reqEmbeddedMIDIConfig) { mpt::IO::Write(f, static_cast(m_MidiCfg)); } // Writing pattern names if(numNamedPats) { mpt::IO::WriteRaw(f, "PNAM", 4); mpt::IO::WriteIntLE(f, numNamedPats * MAX_PATTERNNAME); for(PATTERNINDEX pat = 0; pat < numNamedPats; pat++) { char name[MAX_PATTERNNAME]; mpt::String::WriteBuf(mpt::String::maybeNullTerminated, name) = Patterns[pat].GetName(); mpt::IO::Write(f, name); } } // Writing channel names if(dwChnNamLen && !compatibilityExport) { mpt::IO::WriteRaw(f, "CNAM", 4); mpt::IO::WriteIntLE(f, dwChnNamLen); uint32 nChnNames = dwChnNamLen / MAX_CHANNELNAME; for(uint32 inam = 0; inam < nChnNames; inam++) { char name[MAX_CHANNELNAME]; mpt::String::WriteBuf(mpt::String::maybeNullTerminated, name) = ChnSettings[inam].szName; mpt::IO::Write(f, name); } } // Writing mix plugins info if(!compatibilityExport) { SaveMixPlugins(&f, false); } // Writing song message dwPos = dwHdrPos + dwExtra + (itHeader.insnum + itHeader.smpnum + itHeader.patnum) * 4; if(itHeader.special & ITFileHeader::embedSongMessage) { dwPos += msglength; mpt::IO::WriteRaw(f, m_songMessage.c_str(), msglength); } // Writing instruments const ModInstrument dummyInstr; for(INSTRUMENTINDEX nins = 1; nins <= itHeader.insnum; nins++) { ITInstrumentEx iti; uint32 instSize; const ModInstrument &instr = (Instruments[nins] != nullptr) ? *Instruments[nins] : dummyInstr; instSize = iti.ConvertToIT(instr, compatibilityExport, *this); // Writing instrument inspos[nins - 1] = static_cast(dwPos); dwPos += instSize; mpt::IO::WritePartial(f, iti, instSize); } // Writing dummy sample headers (until we know the correct sample data offset) ITSample itss; MemsetZero(itss); for(SAMPLEINDEX smp = 0; smp < itHeader.smpnum; smp++) { smppos[smp] = static_cast(dwPos); dwPos += sizeof(ITSample); mpt::IO::Write(f, itss); } // Writing Patterns bool needsMptPatSave = false; for(PATTERNINDEX pat = 0; pat < itHeader.patnum; pat++) { uint32 dwPatPos = static_cast(dwPos); if (!Patterns.IsValidPat(pat)) continue; if(Patterns[pat].GetOverrideSignature()) needsMptPatSave = true; // Check for empty pattern if(Patterns[pat].GetNumRows() == 64 && Patterns.IsPatternEmpty(pat)) { patpos[pat] = 0; continue; } patpos[pat] = static_cast(dwPos); // Write pattern header ROWINDEX writeRows = mpt::saturate_cast(Patterns[pat].GetNumRows()); uint16 writeSize = 0; uint16le patinfo[4]; patinfo[0] = 0; patinfo[1] = static_cast(writeRows); patinfo[2] = 0; patinfo[3] = 0; mpt::IO::Write(f, patinfo); dwPos += 8; struct ChnState { ModCommand lastCmd; uint8 mask = 0xFF; }; const CHANNELINDEX maxChannels = std::min(specs.channelsMax, GetNumChannels()); std::vector chnStates(maxChannels); // Maximum 7 bytes per cell, plus end of row marker, so this buffer is always large enough to cover one row. std::vector buf(7 * maxChannels + 1); for(ROWINDEX row = 0; row < writeRows; row++) { uint32 len = 0; const ModCommand *m = Patterns[pat].GetpModCommand(row, 0); for(CHANNELINDEX ch = 0; ch < maxChannels; ch++, m++) { // Skip mptm-specific notes. if(m->IsPcNote()) { needsMptPatSave = true; continue; } auto &chnState = chnStates[ch]; uint8 b = 0; uint8 command = m->command; uint8 param = m->param; uint8 vol = 0xFF; uint8 note = m->note; if (note != NOTE_NONE) b |= 1; if (m->IsNote()) note -= NOTE_MIN; if (note == NOTE_FADE && GetType() != MOD_TYPE_MPT) note = 0xF6; if (m->instr) b |= 2; if (m->volcmd != VOLCMD_NONE) { vol = std::min(m->vol, uint8(9)); switch(m->volcmd) { case VOLCMD_VOLUME: vol = std::min(m->vol, uint8(64)); break; case VOLCMD_PANNING: vol = std::min(m->vol, uint8(64)) + 128; break; case VOLCMD_VOLSLIDEUP: vol += 85; break; case VOLCMD_VOLSLIDEDOWN: vol += 95; break; case VOLCMD_FINEVOLUP: vol += 65; break; case VOLCMD_FINEVOLDOWN: vol += 75; break; case VOLCMD_VIBRATODEPTH: vol += 203; break; case VOLCMD_TONEPORTAMENTO: vol += 193; break; case VOLCMD_PORTADOWN: vol += 105; break; case VOLCMD_PORTAUP: vol += 115; break; case VOLCMD_VIBRATOSPEED: if(command == CMD_NONE) { // Move unsupported command if possible command = CMD_VIBRATO; param = std::min(m->vol, uint8(15)) << 4; vol = 0xFF; } else { vol = 203; } break; case VOLCMD_OFFSET: if(!compatibilityExport) vol += 223; else vol = 0xFF; break; default: vol = 0xFF; } } if (vol != 0xFF) b |= 4; if (command != CMD_NONE) { S3MSaveConvert(command, param, true, compatibilityExport); if (command) b |= 8; } // Packing information if (b) { // Same note ? if (b & 1) { if ((note == chnState.lastCmd.note) && (chnState.lastCmd.volcmd & 1)) { b &= ~1; b |= 0x10; } else { chnState.lastCmd.note = note; chnState.lastCmd.volcmd |= 1; } } // Same instrument ? if (b & 2) { if ((m->instr == chnState.lastCmd.instr) && (chnState.lastCmd.volcmd & 2)) { b &= ~2; b |= 0x20; } else { chnState.lastCmd.instr = m->instr; chnState.lastCmd.volcmd |= 2; } } // Same volume column byte ? if (b & 4) { if ((vol == chnState.lastCmd.vol) && (chnState.lastCmd.volcmd & 4)) { b &= ~4; b |= 0x40; } else { chnState.lastCmd.vol = vol; chnState.lastCmd.volcmd |= 4; } } // Same command / param ? if (b & 8) { if ((command == chnState.lastCmd.command) && (param == chnState.lastCmd.param) && (chnState.lastCmd.volcmd & 8)) { b &= ~8; b |= 0x80; } else { chnState.lastCmd.command = command; chnState.lastCmd.param = param; chnState.lastCmd.volcmd |= 8; } } if (b != chnState.mask) { chnState.mask = b; buf[len++] = static_cast((ch + 1) | IT_bitmask_patternChanEnabled_c); buf[len++] = b; } else { buf[len++] = static_cast(ch + 1); } if (b & 1) buf[len++] = note; if (b & 2) buf[len++] = m->instr; if (b & 4) buf[len++] = vol; if (b & 8) { buf[len++] = command; buf[len++] = param; } } } buf[len++] = 0; if(writeSize > uint16_max - len) { AddToLog(LogWarning, MPT_UFORMAT("Warning: File format limit was reached. Some pattern data may not get written to file. (pattern {})")(pat)); break; } else { dwPos += len; writeSize += (uint16)len; mpt::IO::WriteRaw(f, buf.data(), len); } } mpt::IO::SeekAbsolute(f, dwPatPos); patinfo[0] = writeSize; mpt::IO::Write(f, patinfo); mpt::IO::SeekAbsolute(f, dwPos); } // Writing Sample Data for(SAMPLEINDEX smp = 1; smp <= itHeader.smpnum; smp++) { const ModSample &sample = Samples[smp]; #ifdef MODPLUG_TRACKER uint32 type = GetType() == MOD_TYPE_IT ? 1 : 4; if(compatibilityExport) type = 2; bool compress = ((((sample.GetNumChannels() > 1) ? TrackerSettings::Instance().MiscITCompressionStereo : TrackerSettings::Instance().MiscITCompressionMono) & type) != 0); #else bool compress = false; #endif // MODPLUG_TRACKER // Old MPT, DUMB and probably other libraries will only consider the IT2.15 compression flag if the header version also indicates IT2.15. // MilkyTracker <= 0.90.85 assumes IT2.15 compression with cmwt == 0x215, ignoring the delta flag completely. itss.ConvertToIT(sample, GetType(), compress, itHeader.cmwt >= 0x215, GetType() == MOD_TYPE_MPT); const bool isExternal = itss.cvt == ITSample::cvtExternalSample; mpt::String::WriteBuf(mpt::String::nullTerminated, itss.name) = m_szNames[smp]; itss.samplepointer = static_cast(dwPos); if(dwPos > uint32_max) { // Sample position does not fit into sample pointer! AddToLog(LogWarning, MPT_UFORMAT("Cannot save sample {}: File size exceeds 4 GB.")(smp)); itss.samplepointer = 0; itss.length = 0; } SmpLength smpLength = itss.length; // Possibly truncated to 2^32 samples mpt::IO::SeekAbsolute(f, smppos[smp - 1]); mpt::IO::Write(f, itss); if(dwPos > uint32_max) { continue; } // TODO this actually wraps around at 2 GB, so we either need to use the 64-bit seek API or warn earlier! mpt::IO::SeekAbsolute(f, dwPos); if(!isExternal) { if(sample.nLength > smpLength && smpLength != 0) { // Sample length does not fit into IT header! AddToLog(LogWarning, MPT_UFORMAT("Truncating sample {}: Length exceeds exceeds 4 gigasamples.")(smp)); } dwPos += itss.GetSampleFormat().WriteSample(f, sample, smpLength); } else { #ifdef MPT_EXTERNAL_SAMPLES const std::string filenameU8 = GetSamplePath(smp).AbsolutePathToRelative(filename.GetPath()).ToUTF8(); const size_t strSize = filenameU8.size(); size_t intBytes = 0; if(mpt::IO::WriteVarInt(f, strSize, &intBytes)) { dwPos += intBytes + strSize; mpt::IO::WriteRaw(f, filenameU8.data(), strSize); } #else MPT_UNREFERENCED_PARAMETER(filename); #endif // MPT_EXTERNAL_SAMPLES } } //Save hacked-on extra info if(!compatibilityExport) { if(GetNumInstruments()) { SaveExtendedInstrumentProperties(itHeader.insnum, f); } SaveExtendedSongProperties(f); } // Updating offsets mpt::IO::SeekAbsolute(f, dwHdrPos); mpt::IO::Write(f, inspos); mpt::IO::Write(f, smppos); mpt::IO::Write(f, patpos); if(GetType() == MOD_TYPE_IT) { return true; } //hack //BEGIN: MPT SPECIFIC: bool success = true; mpt::IO::SeekEnd(f); const mpt::IO::Offset MPTStartPos = mpt::IO::TellWrite(f); srlztn::SsbWrite ssb(f); ssb.BeginWrite("mptm", Version::Current().GetRawVersion()); if(GetTuneSpecificTunings().GetNumTunings() > 0 || AreNonDefaultTuningsUsed(*this)) ssb.WriteItem(int8(1), "UTF8Tuning"); if(GetTuneSpecificTunings().GetNumTunings() > 0) ssb.WriteItem(GetTuneSpecificTunings(), "0", &WriteTuningCollection); if(AreNonDefaultTuningsUsed(*this)) ssb.WriteItem(*this, "1", &WriteTuningMap); if(Order().NeedsExtraDatafield()) ssb.WriteItem(Order, "2", &WriteModSequenceOld); if(needsMptPatSave) ssb.WriteItem(Patterns, FileIdPatterns, &WriteModPatterns); ssb.WriteItem(Order, FileIdSequences, &WriteModSequences); ssb.FinishWrite(); if(ssb.GetStatus() & srlztn::SNT_FAILURE) { AddToLog(LogError, U_("Error occurred in writing MPTM extensions.")); } //Last 4 bytes should tell where the hack mpt things begin. if(!f.good()) { f.clear(); success = false; } mpt::IO::WriteIntLE(f, static_cast(MPTStartPos)); mpt::IO::SeekEnd(f); //END : MPT SPECIFIC //NO WRITING HERE ANYMORE. return success; } #endif // MODPLUG_NO_FILESAVE #ifndef MODPLUG_NO_FILESAVE uint32 CSoundFile::SaveMixPlugins(std::ostream *file, bool updatePlugData) { #ifndef NO_PLUGINS uint32 totalSize = 0; for(PLUGINDEX i = 0; i < MAX_MIXPLUGINS; i++) { const SNDMIXPLUGIN &plugin = m_MixPlugins[i]; if(plugin.IsValidPlugin()) { uint32 chunkSize = sizeof(SNDMIXPLUGININFO) + 4; // plugininfo+4 (datalen) if(plugin.pMixPlugin && updatePlugData) { plugin.pMixPlugin->SaveAllParameters(); } const uint32 extraDataSize = 4 + sizeof(float32) + // 4 for ID and size of dryRatio 4 + sizeof(int32); // Default Program // For each extra entity, add 4 for ID, plus 4 for size of entity, plus size of entity chunkSize += extraDataSize + 4; // +4 is for size field itself const uint32 plugDataSize = std::min(mpt::saturate_cast(plugin.pluginData.size()), uint32_max - chunkSize); chunkSize += plugDataSize; if(file) { std::ostream &f = *file; // Chunk ID (= plugin ID) char id[4] = { 'F', 'X', '0', '0' }; if(i >= 100) id[1] = '0' + (i / 100u); id[2] += (i / 10u) % 10u; id[3] += (i % 10u); mpt::IO::WriteRaw(f, id, 4); // Write chunk size, plugin info and plugin data chunk mpt::IO::WriteIntLE(f, chunkSize); mpt::IO::Write(f, m_MixPlugins[i].Info); mpt::IO::WriteIntLE(f, plugDataSize); if(plugDataSize) { mpt::IO::WriteRaw(f, m_MixPlugins[i].pluginData.data(), plugDataSize); } mpt::IO::WriteIntLE(f, extraDataSize); // Dry/Wet ratio mpt::IO::WriteRaw(f, "DWRT", 4); // DWRT chunk does not include a size, so better make sure we always write 4 bytes here. static_assert(sizeof(IEEE754binary32LE) == 4); mpt::IO::Write(f, IEEE754binary32LE(m_MixPlugins[i].fDryRatio)); // Default program mpt::IO::WriteRaw(f, "PROG", 4); // PROG chunk does not include a size, so better make sure we always write 4 bytes here. static_assert(sizeof(m_MixPlugins[i].defaultProgram) == sizeof(int32)); mpt::IO::WriteIntLE(f, m_MixPlugins[i].defaultProgram); // Please, if you add any more chunks here, don't repeat history (see above) and *do* add a size field for your chunk, mmmkay? } totalSize += chunkSize + 8; } } std::vector chinfo(GetNumChannels()); uint32 numChInfo = 0; for(CHANNELINDEX j = 0; j < GetNumChannels(); j++) { if((chinfo[j] = ChnSettings[j].nMixPlugin) != 0) { numChInfo = j + 1; } } if(numChInfo) { if(file) { std::ostream &f = *file; mpt::IO::WriteRaw(f, "CHFX", 4); mpt::IO::WriteIntLE(f, numChInfo * 4); chinfo.resize(numChInfo); mpt::IO::Write(f, chinfo); } totalSize += numChInfo * 4 + 8; } return totalSize; #else MPT_UNREFERENCED_PARAMETER(file); MPT_UNREFERENCED_PARAMETER(updatePlugData); return 0; #endif // NO_PLUGINS } #endif // MODPLUG_NO_FILESAVE bool CSoundFile::LoadMixPlugins(FileReader &file) { bool isBeRoTracker = false; while(file.CanRead(9)) { char code[4]; file.ReadArray(code); const uint32 chunkSize = file.ReadUint32LE(); if(!memcmp(code, "IMPI", 4) // IT instrument, we definitely read too far || !memcmp(code, "IMPS", 4) // IT sample, ditto || !memcmp(code, "XTPM", 4) // Instrument extensions, ditto || !memcmp(code, "STPM", 4) // Song extensions, ditto || !file.CanRead(chunkSize)) { file.SkipBack(8); return isBeRoTracker; } FileReader chunk = file.ReadChunk(chunkSize); // Channel FX if(!memcmp(code, "CHFX", 4)) { for(auto &chn : ChnSettings) { chn.nMixPlugin = static_cast(chunk.ReadUint32LE()); } #ifndef NO_PLUGINS } // Plugin Data FX00, ... FX99, F100, ... F255 #define MPT_ISDIGIT(x) (code[(x)] >= '0' && code[(x)] <= '9') else if(code[0] == 'F' && (code[1] == 'X' || MPT_ISDIGIT(1)) && MPT_ISDIGIT(2) && MPT_ISDIGIT(3)) #undef MPT_ISDIGIT { PLUGINDEX plug = (code[2] - '0') * 10 + (code[3] - '0'); //calculate plug-in number. if(code[1] != 'X') plug += (code[1] - '0') * 100; if(plug < MAX_MIXPLUGINS) { ReadMixPluginChunk(chunk, m_MixPlugins[plug]); } #endif // NO_PLUGINS } else if(!memcmp(code, "MODU", 4)) { isBeRoTracker = true; m_dwLastSavedWithVersion = Version(); // Reset MPT detection for old files that have a similar fingerprint } } return isBeRoTracker; } #ifndef NO_PLUGINS void CSoundFile::ReadMixPluginChunk(FileReader &file, SNDMIXPLUGIN &plugin) { // MPT's standard plugin data. Size not specified in file.. grrr.. file.ReadStruct(plugin.Info); mpt::String::SetNullTerminator(plugin.Info.szName.buf); mpt::String::SetNullTerminator(plugin.Info.szLibraryName.buf); plugin.editorX = plugin.editorY = int32_min; // Plugin user data FileReader pluginDataChunk = file.ReadChunk(file.ReadUint32LE()); plugin.pluginData.resize(mpt::saturate_cast(pluginDataChunk.BytesLeft())); pluginDataChunk.ReadRaw(mpt::as_span(plugin.pluginData)); if(FileReader modularData = file.ReadChunk(file.ReadUint32LE()); modularData.IsValid()) { while(modularData.CanRead(5)) { // do we recognize this chunk? char code[4]; modularData.ReadArray(code); uint32 dataSize = 0; if(!memcmp(code, "DWRT", 4) || !memcmp(code, "PROG", 4)) { // Legacy system with fixed size chunks dataSize = 4; } else { dataSize = modularData.ReadUint32LE(); } FileReader dataChunk = modularData.ReadChunk(dataSize); if(!memcmp(code, "DWRT", 4)) { plugin.fDryRatio = std::clamp(dataChunk.ReadFloatLE(), 0.0f, 1.0f); if(!std::isnormal(plugin.fDryRatio)) plugin.fDryRatio = 0.0f; } else if(!memcmp(code, "PROG", 4)) { plugin.defaultProgram = dataChunk.ReadUint32LE(); } else if(!memcmp(code, "MCRO", 4)) { // Read plugin-specific macros //dataChunk.ReadStructPartial(plugin.macros, dataChunk.GetLength()); } } } } #endif // NO_PLUGINS #ifndef MODPLUG_NO_FILESAVE void CSoundFile::SaveExtendedSongProperties(std::ostream &f) const { const CModSpecifications &specs = GetModSpecifications(); // Extra song data - Yet Another Hack. mpt::IO::WriteIntLE(f, MagicBE("MPTS")); #define WRITEMODULARHEADER(code, fsize) \ { \ mpt::IO::WriteIntLE(f, code); \ MPT_ASSERT(mpt::in_range(fsize)); \ const uint16 _size = fsize; \ mpt::IO::WriteIntLE(f, _size); \ } #define WRITEMODULAR(code, field) \ { \ WRITEMODULARHEADER(code, sizeof(field)) \ mpt::IO::WriteIntLE(f, field); \ } if(m_nDefaultTempo.GetInt() > 255) { uint32 tempo = m_nDefaultTempo.GetInt(); WRITEMODULAR(MagicBE("DT.."), tempo); } if(m_nDefaultTempo.GetFract() != 0 && specs.hasFractionalTempo) { uint32 tempo = m_nDefaultTempo.GetFract(); WRITEMODULAR(MagicLE("DTFR"), tempo); } if(m_nDefaultRowsPerBeat > 255 || m_nDefaultRowsPerMeasure > 255 || GetType() == MOD_TYPE_XM) { WRITEMODULAR(MagicBE("RPB."), m_nDefaultRowsPerBeat); WRITEMODULAR(MagicBE("RPM."), m_nDefaultRowsPerMeasure); } if(GetType() != MOD_TYPE_XM) { WRITEMODULAR(MagicBE("C..."), m_nChannels); } if((GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) && GetNumChannels() > 64) { // IT header has only room for 64 channels. Save the settings that do not fit to the header here as an extension. WRITEMODULARHEADER(MagicBE("ChnS"), (GetNumChannels() - 64) * 2); for(CHANNELINDEX chn = 64; chn < GetNumChannels(); chn++) { uint8 panvol[2]; panvol[0] = (uint8)(ChnSettings[chn].nPan >> 2); if (ChnSettings[chn].dwFlags[CHN_SURROUND]) panvol[0] = 100; if (ChnSettings[chn].dwFlags[CHN_MUTE]) panvol[0] |= 0x80; panvol[1] = (uint8)ChnSettings[chn].nVolume; mpt::IO::Write(f, panvol); } } { WRITEMODULARHEADER(MagicBE("TM.."), 1); uint8 mode = static_cast(m_nTempoMode); mpt::IO::WriteIntLE(f, mode); } const int32 tmpMixLevels = static_cast(m_nMixLevels); WRITEMODULAR(MagicBE("PMM."), tmpMixLevels); if(m_dwCreatedWithVersion) { WRITEMODULAR(MagicBE("CWV."), m_dwCreatedWithVersion.GetRawVersion()); } WRITEMODULAR(MagicBE("LSWV"), Version::Current().GetRawVersion()); WRITEMODULAR(MagicBE("SPA."), m_nSamplePreAmp); WRITEMODULAR(MagicBE("VSTV"), m_nVSTiVolume); if(GetType() == MOD_TYPE_XM && m_nDefaultGlobalVolume != MAX_GLOBAL_VOLUME) { WRITEMODULAR(MagicBE("DGV."), m_nDefaultGlobalVolume); } if(GetType() != MOD_TYPE_XM && Order().GetRestartPos() != 0) { WRITEMODULAR(MagicBE("RP.."), Order().GetRestartPos()); } if(m_nResampling != SRCMODE_DEFAULT && specs.hasDefaultResampling) { WRITEMODULAR(MagicLE("RSMP"), static_cast(m_nResampling)); } // Sample cues if(GetType() == MOD_TYPE_MPT) { for(SAMPLEINDEX smp = 1; smp <= GetNumSamples(); smp++) { const ModSample &sample = Samples[smp]; if(sample.nLength && sample.HasCustomCuePoints()) { // Write one chunk for every sample. // Rationale: chunks are limited to 65536 bytes, which can easily be reached // with the amount of samples that OpenMPT supports. WRITEMODULARHEADER(MagicLE("CUES"), static_cast(2 + std::size(sample.cues) * 4)); mpt::IO::WriteIntLE(f, smp); for(auto cue : sample.cues) { mpt::IO::WriteIntLE(f, cue); } } } } // Tempo Swing Factors if(!m_tempoSwing.empty()) { std::ostringstream oStrm; TempoSwing::Serialize(oStrm, m_tempoSwing); std::string data = oStrm.str(); uint16 length = mpt::saturate_cast(data.size()); WRITEMODULARHEADER(MagicLE("SWNG"), length); mpt::IO::WriteRaw(f, data.data(), length); } // Playback compatibility flags { uint8 bits[(kMaxPlayBehaviours + 7) / 8u]; MemsetZero(bits); size_t maxBit = 0; for(size_t i = 0; i < kMaxPlayBehaviours; i++) { if(m_playBehaviour[i]) { bits[i >> 3] |= 1 << (i & 0x07); maxBit = i + 8; } } uint16 numBytes = static_cast(maxBit / 8u); WRITEMODULARHEADER(MagicBE("MSF."), numBytes); mpt::IO::WriteRaw(f, bits, numBytes); } if(!m_songArtist.empty() && specs.hasArtistName) { std::string songArtistU8 = mpt::ToCharset(mpt::Charset::UTF8, m_songArtist); uint16 length = mpt::saturate_cast(songArtistU8.length()); WRITEMODULARHEADER(MagicLE("AUTH"), length); mpt::IO::WriteRaw(f, songArtistU8.c_str(), length); } #ifdef MODPLUG_TRACKER // MIDI mapping directives if(GetMIDIMapper().GetCount() > 0) { const size_t objectsize = GetMIDIMapper().Serialize(); if(!mpt::in_range(objectsize)) { AddToLog(LogWarning, U_("Too many MIDI Mapping directives to save; data won't be written.")); } else { WRITEMODULARHEADER(MagicBE("MIMA"), static_cast(objectsize)); GetMIDIMapper().Serialize(&f); } } // Channel colors { CHANNELINDEX numChannels = 0; for(CHANNELINDEX i = 0; i < m_nChannels; i++) { if(ChnSettings[i].color != ModChannelSettings::INVALID_COLOR) { numChannels = i + 1; } } if(numChannels > 0) { WRITEMODULARHEADER(MagicLE("CCOL"), numChannels * 4); for(CHANNELINDEX i = 0; i < numChannels; i++) { uint32 color = ChnSettings[i].color; if(color != ModChannelSettings::INVALID_COLOR) color &= 0x00FFFFFF; std::array rgb{static_cast(color), static_cast(color >> 8), static_cast(color >> 16), static_cast(color >> 24)}; mpt::IO::Write(f, rgb); } } } #endif #undef WRITEMODULAR #undef WRITEMODULARHEADER return; } #endif // MODPLUG_NO_FILESAVE template void ReadField(FileReader &chunk, std::size_t size, T &field) { field = chunk.ReadSizedIntLE(size); } template void ReadFieldCast(FileReader &chunk, std::size_t size, T &field) { static_assert(sizeof(T) <= sizeof(int32)); field = static_cast(chunk.ReadSizedIntLE(size)); } void CSoundFile::LoadExtendedSongProperties(FileReader &file, bool ignoreChannelCount, bool *pInterpretMptMade) { if(!file.ReadMagic("STPM")) // 'MPTS' { return; } // Found MPTS, interpret the file MPT made. if(pInterpretMptMade != nullptr) *pInterpretMptMade = true; // HACK: Reset mod flags to default values here, as they are not always written. m_playBehaviour.reset(); while(file.CanRead(7)) { const uint32 code = file.ReadUint32LE(); const uint16 size = file.ReadUint16LE(); // Start of MPTM extensions, non-ASCII ID or truncated field if(code == MagicLE("228\x04")) { file.SkipBack(6); break; } else if((code & 0x80808080) || !(code & 0x60606060) || !file.CanRead(size)) { break; } FileReader chunk = file.ReadChunk(size); switch (code) // interpret field code { case MagicBE("DT.."): { uint32 tempo; ReadField(chunk, size, tempo); m_nDefaultTempo.Set(tempo, m_nDefaultTempo.GetFract()); break; } case MagicLE("DTFR"): { uint32 tempoFract; ReadField(chunk, size, tempoFract); m_nDefaultTempo.Set(m_nDefaultTempo.GetInt(), tempoFract); break; } case MagicBE("RPB."): ReadField(chunk, size, m_nDefaultRowsPerBeat); break; case MagicBE("RPM."): ReadField(chunk, size, m_nDefaultRowsPerMeasure); break; // FIXME: If there are only PC events on the last few channels in an MPTM MO3, they won't be imported! case MagicBE("C..."): if(!ignoreChannelCount) { CHANNELINDEX chn = 0; ReadField(chunk, size, chn); m_nChannels = Clamp(chn, m_nChannels, MAX_BASECHANNELS); } break; case MagicBE("TM.."): ReadFieldCast(chunk, size, m_nTempoMode); break; case MagicBE("PMM."): ReadFieldCast(chunk, size, m_nMixLevels); break; case MagicBE("CWV."): { uint32 ver = 0; ReadField(chunk, size, ver); m_dwCreatedWithVersion = Version(ver); break; } case MagicBE("LSWV"): { uint32 ver = 0; ReadField(chunk, size, ver); if(ver != 0) { m_dwLastSavedWithVersion = Version(ver); } break; } case MagicBE("SPA."): ReadField(chunk, size, m_nSamplePreAmp); break; case MagicBE("VSTV"): ReadField(chunk, size, m_nVSTiVolume); break; case MagicBE("DGV."): ReadField(chunk, size, m_nDefaultGlobalVolume); break; case MagicBE("RP.."): if(GetType() != MOD_TYPE_XM) { ORDERINDEX restartPos; ReadField(chunk, size, restartPos); Order().SetRestartPos(restartPos); } break; case MagicLE("RSMP"): ReadFieldCast(chunk, size, m_nResampling); if(!Resampling::IsKnownMode(m_nResampling)) m_nResampling = SRCMODE_DEFAULT; break; #ifdef MODPLUG_TRACKER case MagicBE("MIMA"): GetMIDIMapper().Deserialize(chunk); break; case MagicLE("CCOL"): // Channel colors { const CHANNELINDEX numChannels = std::min(MAX_BASECHANNELS, static_cast(size / 4u)); for(CHANNELINDEX i = 0; i < numChannels; i++) { auto rgb = chunk.ReadArray(); if(rgb[3]) ChnSettings[i].color = ModChannelSettings::INVALID_COLOR; else ChnSettings[i].color = rgb[0] | (rgb[1] << 8) | (rgb[2] << 16); } } break; #endif case MagicLE("AUTH"): { std::string artist; chunk.ReadString(artist, chunk.GetLength()); m_songArtist = mpt::ToUnicode(mpt::Charset::UTF8, artist); } break; case MagicBE("ChnS"): // Channel settings for channels 65+ if(size <= (MAX_BASECHANNELS - 64) * 2 && (size % 2u) == 0) { static_assert(mpt::array_size::size >= 64); const CHANNELINDEX loopLimit = std::min(uint16(64 + size / 2), uint16(std::size(ChnSettings))); for(CHANNELINDEX chn = 64; chn < loopLimit; chn++) { auto [pan, vol] = chunk.ReadArray(); if(pan != 0xFF) { ChnSettings[chn].nVolume = vol; ChnSettings[chn].nPan = 128; ChnSettings[chn].dwFlags.reset(); if(pan & 0x80) ChnSettings[chn].dwFlags.set(CHN_MUTE); pan &= 0x7F; if(pan <= 64) ChnSettings[chn].nPan = pan << 2; if(pan == 100) ChnSettings[chn].dwFlags.set(CHN_SURROUND); } } } break; case MagicLE("CUES"): // Sample cues if(size > 2) { SAMPLEINDEX smp = chunk.ReadUint16LE(); if(smp > 0 && smp <= GetNumSamples()) { ModSample &sample = Samples[smp]; for(auto &cue : sample.cues) { if(chunk.CanRead(4)) cue = chunk.ReadUint32LE(); else cue = MAX_SAMPLE_LENGTH; } } } break; case MagicLE("SWNG"): // Tempo Swing Factors if(size > 2) { std::istringstream iStrm(mpt::buffer_cast(chunk.ReadRawDataAsByteVector())); TempoSwing::Deserialize(iStrm, m_tempoSwing, chunk.GetLength()); } break; case MagicBE("MSF."): // Playback compatibility flags { size_t bit = 0; m_playBehaviour.reset(); while(chunk.CanRead(1) && bit < m_playBehaviour.size()) { uint8 b = chunk.ReadUint8(); for(uint8 i = 0; i < 8; i++, bit++) { if((b & (1 << i)) && bit < m_playBehaviour.size()) { m_playBehaviour.set(bit); } } } } break; } } // Validate read values. Limit(m_nDefaultTempo, GetModSpecifications().GetTempoMin(), GetModSpecifications().GetTempoMax()); if(m_nTempoMode >= TempoMode::NumModes) m_nTempoMode = TempoMode::Classic; if(m_nMixLevels >= MixLevels::NumMixLevels) m_nMixLevels = MixLevels::Original; //m_dwCreatedWithVersion //m_dwLastSavedWithVersion //m_nSamplePreAmp //m_nVSTiVolume //m_nDefaultGlobalVolume LimitMax(m_nDefaultGlobalVolume, MAX_GLOBAL_VOLUME); //m_nRestartPos //m_ModFlags LimitMax(m_nDefaultRowsPerBeat, MAX_ROWS_PER_BEAT); LimitMax(m_nDefaultRowsPerMeasure, MAX_ROWS_PER_BEAT); } OPENMPT_NAMESPACE_END