diff --git a/Frameworks/OpenMPT/OpenMPT/build/dist.mk b/Frameworks/OpenMPT/OpenMPT/build/dist.mk index 9a8f1c9f4..a6bed4b06 100644 --- a/Frameworks/OpenMPT/OpenMPT/build/dist.mk +++ b/Frameworks/OpenMPT/OpenMPT/build/dist.mk @@ -1,4 +1,4 @@ -MPT_SVNVERSION=21953 -MPT_SVNURL=https://source.openmpt.org/svn/openmpt/tags/libopenmpt-0.7.11 -MPT_SVNDATE=2024-10-26T13:09:27.804941Z +MPT_SVNVERSION=22406 +MPT_SVNURL=https://source.openmpt.org/svn/openmpt/tags/libopenmpt-0.7.12 +MPT_SVNDATE=2024-12-01T13:10:15.135688Z diff --git a/Frameworks/OpenMPT/OpenMPT/build/make/config-djgpp.mk b/Frameworks/OpenMPT/OpenMPT/build/make/config-djgpp.mk index c522b6a20..26683ff25 100644 --- a/Frameworks/OpenMPT/OpenMPT/build/make/config-djgpp.mk +++ b/Frameworks/OpenMPT/OpenMPT/build/make/config-djgpp.mk @@ -53,7 +53,8 @@ ifneq ($(SSE),0) FPU_SSSE3 := -m80387 -mmmx -mfxsr -msse -msse2 -msse3 -mssse3 -mfpmath=sse FPU_SSE4_1 := -m80387 -mmmx -mfxsr -msse -msse2 -msse3 -mssse3 -msse4.1 -mfpmath=sse FPU_SSE4_2 := -m80387 -mmmx -mfxsr -msse -msse2 -msse3 -mssse3 -msse4.1 -msse4.2 -mfpmath=sse - FPU_SSE4A := -m80387 -mmmx -mfxsr -msse -msse2 -msse3 -mssse3 -msse4a -mfpmath=sse + FPU_SSE4A := -m80387 -mmmx -mfxsr -msse -msse2 -msse3 -msse4a -mfpmath=sse + FPU_SSSE4A := -m80387 -mmmx -mfxsr -msse -msse2 -msse3 -mssse3 -msse4a -mfpmath=sse else FPU_NONE := -mno-80387 FPU_287 := -m80387 -mfpmath=387 -mno-fancy-math-387 @@ -68,7 +69,8 @@ else FPU_SSSE3 := -mno-ssse3 -mno-sse3 -mno-sse2 -mno-sse -mno-fxsr -m80387 -mmmx -mfpmath=387 FPU_SSE4_1 := -mno-sse4.1 -mno-ssse3 -mno-sse3 -mno-sse2 -mno-sse -mno-fxsr -m80387 -mmmx -mfpmath=387 FPU_SSE4_2 := -mno-sse4.2 -mno-sse4.1 -mno-ssse3 -mno-sse3 -mno-sse2 -mno-sse -mno-fxsr -m80387 -mmmx -mfpmath=387 - FPU_SSE4A := -mno-sse4a -mno-ssse3 -mno-sse3 -mno-sse2 -mno-sse -mno-fxsr -m80387 -mmmx -mfpmath=387 + FPU_SSE4A := -mno-sse4a -mno-sse3 -mno-sse2 -mno-sse -mno-fxsr -m80387 -mmmx -mfpmath=387 + FPU_SSSE4A := -mno-sse4a -mno-ssse3 -mno-sse3 -mno-sse2 -mno-sse -mno-fxsr -m80387 -mmmx -mfpmath=387 endif OPT_DEF := -Os @@ -202,11 +204,11 @@ amd/sempron64 := $(___) -march=k8 $(FPU_SSE2) -mtune=k8 amd/geode-gx := $(___) -march=geode $(FPU_3DNOW) -mtune=geode $(OPT_SIMD) --param l1-cache-size=16 --param l2-cache-size=0 amd/geode-lx := $(___) -march=geode $(FPU_3DNOW) -mtune=geode $(OPT_SIMD) --param l1-cache-size=64 --param l2-cache-size=128 amd/geode-nx := $(___) -march=athlon-xp $(FPU_3DASSE) -mtune=athlon-xp $(OPT_SIMD) --param l1-cache-size=64 --param l2-cache-size=256 -amd/bobcat := $(___) -march=btver1 $(FPU_SSE4A) -mtune=btver1 $(OPT_SIMD) --param l1-cache-size=32 --param l2-cache-size=512 -amd/jaguar := $(___) -march=btver2 $(FPU_SSE4A) -mtune=btver2 $(OPT_SIMD) --param l1-cache-size=32 --param l2-cache-size=1024 +amd/bobcat := $(___) -march=btver1 $(FPU_SSSE4A) -mtune=btver1 $(OPT_SIMD) --param l1-cache-size=32 --param l2-cache-size=512 +amd/jaguar := $(___) -march=btver2 $(FPU_SSSE4A) -mtune=btver2 $(OPT_SIMD) --param l1-cache-size=32 --param l2-cache-size=1024 amd/late-3dnow := $(XX_) -march=athlon-xp $(FPU_3DASSE) -mtune=athlon-xp $(OPT_SIMD) --param l1-cache-size=64 --param l2-cache-size=512 -amd/late := $(XX_) -march=i686 $(FPU_SSE4A) -mtune=generic $(OPT_SIMD) +amd/late := $(XX_) -march=i686 $(FPU_SSSE4A) -mtune=generic $(OPT_SIMD) diff --git a/Frameworks/OpenMPT/OpenMPT/build/svn_version/svn_version.h b/Frameworks/OpenMPT/OpenMPT/build/svn_version/svn_version.h index 6990a182d..55a863d78 100644 --- a/Frameworks/OpenMPT/OpenMPT/build/svn_version/svn_version.h +++ b/Frameworks/OpenMPT/OpenMPT/build/svn_version/svn_version.h @@ -1,10 +1,10 @@ #pragma once -#define OPENMPT_VERSION_SVNVERSION "21953" -#define OPENMPT_VERSION_REVISION 21953 +#define OPENMPT_VERSION_SVNVERSION "22406" +#define OPENMPT_VERSION_REVISION 22406 #define OPENMPT_VERSION_DIRTY 0 #define OPENMPT_VERSION_MIXEDREVISIONS 0 -#define OPENMPT_VERSION_URL "https://source.openmpt.org/svn/openmpt/tags/libopenmpt-0.7.11" -#define OPENMPT_VERSION_DATE "2024-10-26T13:09:27.804941Z" +#define OPENMPT_VERSION_URL "https://source.openmpt.org/svn/openmpt/tags/libopenmpt-0.7.12" +#define OPENMPT_VERSION_DATE "2024-12-01T13:10:15.135688Z" #define OPENMPT_VERSION_IS_PACKAGE 1 diff --git a/Frameworks/OpenMPT/OpenMPT/common/mptFileTemporary.cpp b/Frameworks/OpenMPT/OpenMPT/common/mptFileTemporary.cpp index 72894e8c2..8b3330197 100644 --- a/Frameworks/OpenMPT/OpenMPT/common/mptFileTemporary.cpp +++ b/Frameworks/OpenMPT/OpenMPT/common/mptFileTemporary.cpp @@ -68,7 +68,7 @@ TempFileGuard::~TempFileGuard() { if(!filename.empty()) { - DeleteFile(filename.AsNative().c_str()); + DeleteFile(mpt::support_long_path(filename.AsNative()).c_str()); } } @@ -80,7 +80,7 @@ TempDirGuard::TempDirGuard(const mpt::TemporaryPathname &pathname) { return; } - if(::CreateDirectory(dirname.AsNative().c_str(), NULL) == 0) + if(::CreateDirectory(mpt::support_long_path(dirname.AsNative()).c_str(), NULL) == 0) { // fail dirname = mpt::PathString(); } diff --git a/Frameworks/OpenMPT/OpenMPT/common/mptPathString.cpp b/Frameworks/OpenMPT/OpenMPT/common/mptPathString.cpp index ac677d2f4..2ab02faca 100644 --- a/Frameworks/OpenMPT/OpenMPT/common/mptPathString.cpp +++ b/Frameworks/OpenMPT/OpenMPT/common/mptPathString.cpp @@ -55,7 +55,7 @@ mpt::PathString AbsolutePathToRelative(const mpt::PathString &path, const mpt::P using namespace path_literals; using char_type = RawPathString::value_type; mpt::PathString result = path; - if(path.empty()) + if(path.empty() || relativeTo.empty()) { return result; } @@ -79,7 +79,7 @@ mpt::PathString RelativePathToAbsolute(const mpt::PathString &path, const mpt::P using namespace path_literals; using char_type = RawPathString::value_type; mpt::PathString result = path; - if(path.empty()) + if(path.empty() || relativeTo.empty()) { return result; } diff --git a/Frameworks/OpenMPT/OpenMPT/common/version.cpp b/Frameworks/OpenMPT/OpenMPT/common/version.cpp index 6e47945bd..a8b444c47 100644 --- a/Frameworks/OpenMPT/OpenMPT/common/version.cpp +++ b/Frameworks/OpenMPT/OpenMPT/common/version.cpp @@ -621,6 +621,7 @@ mpt::ustring GetFullCreditsString() "Revenant (https://revenant1.net/)\n" "SYRiNX\n" "xaimus (http://xaimus.com/)\n" + "zersal\n" "\n" "Thanks to:\n" "\n" @@ -660,7 +661,7 @@ mpt::ustring GetFullCreditsString() "https://github.com/iamgreaser/it2everything/\n" "\n" "Antti S. Lankila for Amiga resampler implementation\n" - "https://bel.fi/alankila/modguide/interpolate.txt\n" + "https://web.archive.org/web/20221228071135/https://bel.fi/alankila/modguide/\n" "\n" "Shayde / Reality Productions for Opal OPL3 emulator\n" "https://www.3eality.com/\n" @@ -774,7 +775,7 @@ mpt::ustring GetFullCreditsString() "https://www.behance.net/ulfurkolka\n" "\n" "Nobuyuki for file icon\n" - "https://twitter.com/nobuyukinyuu\n" + "https://github.com/nobuyukinyuu/\n" "\n" #endif "Daniel Collin (emoon/TBL) for providing test infrastructure\n" @@ -784,8 +785,8 @@ mpt::ustring GetFullCreditsString() "in the form of ideas, testing and support;\n" "thanks particularly to:\n" "33, 8bitbubsy, AliceLR, Anboi, BooT-SectoR-ViruZ, Bvanoudtshoorn\n" - "christofori, cubaxd, Diamond, Ganja, Georg, Goor00,\n" - "Harbinger, jmkz, KrazyKatz, LPChip, Nofold, Rakib, Sam Zen\n" + "a11cf0, christofori, cubaxd, Diamond, Ganja, Georg, Goor00,\n" + "Harbinger, jmkz, KrazyKatz, LPChip, MiDoRi, Nofold, Rakib, Sam Zen\n" "Skaven, Skilletaudio, Snu, Squirrel Havoc, Teimoso, Waxhead\n" "\n" #ifdef MPT_WITH_VST diff --git a/Frameworks/OpenMPT/OpenMPT/common/versionNumber.h b/Frameworks/OpenMPT/OpenMPT/common/versionNumber.h index bc351c444..29f85967c 100644 --- a/Frameworks/OpenMPT/OpenMPT/common/versionNumber.h +++ b/Frameworks/OpenMPT/OpenMPT/common/versionNumber.h @@ -17,7 +17,7 @@ OPENMPT_NAMESPACE_BEGIN // Version definitions. The only thing that needs to be changed when changing version number. #define VER_MAJORMAJOR 1 #define VER_MAJOR 31 -#define VER_MINOR 11 +#define VER_MINOR 13 #define VER_MINORMINOR 00 OPENMPT_NAMESPACE_END diff --git a/Frameworks/OpenMPT/OpenMPT/libopenmpt/libopenmpt_ext_impl.cpp b/Frameworks/OpenMPT/OpenMPT/libopenmpt/libopenmpt_ext_impl.cpp index d108290b9..274ccf4e6 100644 --- a/Frameworks/OpenMPT/OpenMPT/libopenmpt/libopenmpt_ext_impl.cpp +++ b/Frameworks/OpenMPT/OpenMPT/libopenmpt/libopenmpt_ext_impl.cpp @@ -169,11 +169,11 @@ namespace openmpt { if ( volume < 0.0 || volume > 1.0 ) { throw openmpt::exception("invalid global volume"); } - m_sndFile->m_PlayState.m_nGlobalVolume = mpt::saturate_round( volume * MAX_GLOBAL_VOLUME ); + m_sndFile->m_PlayState.m_nGlobalVolume = mpt::saturate_round( volume * OpenMPT::MAX_GLOBAL_VOLUME ); } double module_ext_impl::get_global_volume( ) const { - return m_sndFile->m_PlayState.m_nGlobalVolume / static_cast( MAX_GLOBAL_VOLUME ); + return m_sndFile->m_PlayState.m_nGlobalVolume / static_cast( OpenMPT::MAX_GLOBAL_VOLUME ); } void module_ext_impl::set_channel_volume( std::int32_t channel, double volume ) { diff --git a/Frameworks/OpenMPT/OpenMPT/libopenmpt/libopenmpt_version.h b/Frameworks/OpenMPT/OpenMPT/libopenmpt/libopenmpt_version.h index f64a0f7dc..ac2640567 100644 --- a/Frameworks/OpenMPT/OpenMPT/libopenmpt/libopenmpt_version.h +++ b/Frameworks/OpenMPT/OpenMPT/libopenmpt/libopenmpt_version.h @@ -21,7 +21,7 @@ /*! \brief libopenmpt minor version number */ #define OPENMPT_API_VERSION_MINOR 7 /*! \brief libopenmpt patch version number */ -#define OPENMPT_API_VERSION_PATCH 11 +#define OPENMPT_API_VERSION_PATCH 12 /*! \brief libopenmpt pre-release tag */ #define OPENMPT_API_VERSION_PREREL "" /*! \brief libopenmpt pre-release flag */ diff --git a/Frameworks/OpenMPT/OpenMPT/libopenmpt/libopenmpt_version.mk b/Frameworks/OpenMPT/OpenMPT/libopenmpt/libopenmpt_version.mk index 81742762e..a1b0f4e02 100644 --- a/Frameworks/OpenMPT/OpenMPT/libopenmpt/libopenmpt_version.mk +++ b/Frameworks/OpenMPT/OpenMPT/libopenmpt/libopenmpt_version.mk @@ -1,8 +1,8 @@ LIBOPENMPT_VERSION_MAJOR=0 LIBOPENMPT_VERSION_MINOR=7 -LIBOPENMPT_VERSION_PATCH=11 +LIBOPENMPT_VERSION_PATCH=12 LIBOPENMPT_VERSION_PREREL= LIBOPENMPT_LTVER_CURRENT=4 -LIBOPENMPT_LTVER_REVISION=11 +LIBOPENMPT_LTVER_REVISION=12 LIBOPENMPT_LTVER_AGE=4 diff --git a/Frameworks/OpenMPT/OpenMPT/soundlib/ContainerMMCMP.cpp b/Frameworks/OpenMPT/OpenMPT/soundlib/ContainerMMCMP.cpp index 74d988134..d61eb2d1e 100644 --- a/Frameworks/OpenMPT/OpenMPT/soundlib/ContainerMMCMP.cpp +++ b/Frameworks/OpenMPT/OpenMPT/soundlib/ContainerMMCMP.cpp @@ -231,6 +231,7 @@ bool UnpackMMCMP(std::vector &containerItems, FileReader &file, C #ifdef MMCMP_LOG MPT_LOG_GLOBAL(LogDebug, "MMCMP", MPT_UFORMAT(" 16-bit block: pos={} size={} {} {}")(psubblk->position, psubblk->size, (blk.flags & MMCMP_DELTA) ? U_("DELTA ") : U_(""), (blk.flags & MMCMP_ABS16) ? U_("ABS16 ") : U_(""))); #endif + if(numbits > 15) return false; if(!file.Seek(memPos + blk.tt_entries)) return false; if(!file.CanRead(blk.pk_size - blk.tt_entries)) return false; BitReader bitFile{ file.GetChunk(blk.pk_size - blk.tt_entries) }; @@ -316,6 +317,7 @@ bool UnpackMMCMP(std::vector &containerItems, FileReader &file, C uint32 numbits = blk.num_bits; uint32 oldval = 0; if(blk.tt_entries > sizeof(ptable) + || numbits > 7 || !file.Seek(memPos) || file.ReadRaw(mpt::span(ptable, blk.tt_entries)).size() < blk.tt_entries) return false; diff --git a/Frameworks/OpenMPT/OpenMPT/soundlib/Dlsbank.cpp b/Frameworks/OpenMPT/OpenMPT/soundlib/Dlsbank.cpp index 8a4eca312..b0b08cf12 100644 --- a/Frameworks/OpenMPT/OpenMPT/soundlib/Dlsbank.cpp +++ b/Frameworks/OpenMPT/OpenMPT/soundlib/Dlsbank.cpp @@ -2127,9 +2127,9 @@ bool CDLSBank::ExtractInstrument(CSoundFile &sndFile, INSTRUMENTINDEX nInstr, ui } } pIns->nFadeOut = 1024; - pIns->nMidiProgram = (uint8)(dlsIns.ulInstrument & 0x7F) + 1; - pIns->nMidiChannel = (uint8)(isDrum ? 10 : 0); - pIns->wMidiBank = (uint16)(((dlsIns.ulBank & 0x7F00) >> 1) | (dlsIns.ulBank & 0x7F)); + pIns->nMidiProgram = static_cast(1 + (dlsIns.ulInstrument & 0x7F)); + pIns->nMidiChannel = static_cast(isDrum ? 10 : 0); + pIns->wMidiBank = static_cast(1 + (((dlsIns.ulBank & 0x7F00) >> 1) | (dlsIns.ulBank & 0x7F))); pIns->nNNA = NewNoteAction::NoteOff; pIns->nDCT = DuplicateCheckType::Note; pIns->nDNA = DuplicateNoteAction::NoteFade; @@ -2146,7 +2146,7 @@ bool CDLSBank::ExtractInstrument(CSoundFile &sndFile, INSTRUMENTINDEX nInstr, ui const DLSREGION &rgn = dlsIns.Regions[nRgn]; if(rgn.IsDummy()) continue; - // Elimitate Duplicate Regions + // Eliminate Duplicate Regions uint32 dupRegion; for(dupRegion = minRegion; dupRegion < nRgn; dupRegion++) { diff --git a/Frameworks/OpenMPT/OpenMPT/soundlib/ITTools.cpp b/Frameworks/OpenMPT/OpenMPT/soundlib/ITTools.cpp index 898f247e9..527d53c19 100644 --- a/Frameworks/OpenMPT/OpenMPT/soundlib/ITTools.cpp +++ b/Frameworks/OpenMPT/OpenMPT/soundlib/ITTools.cpp @@ -530,10 +530,9 @@ void ITSample::ConvertToIT(const ModSample &mptSmp, MODTYPE fromType, bool compr // Convert an ITSample to OpenMPT's internal sample representation. uint32 ITSample::ConvertToMPT(ModSample &mptSmp) const { - if(memcmp(id, "IMPS", 4)) - { - return 0; - } + // 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. + //if(memcmp(id, "IMPS", 4)) + // return 0; mptSmp.Initialize(MOD_TYPE_IT); mptSmp.SetDefaultCuePoints(); // For old IT/MPTM files diff --git a/Frameworks/OpenMPT/OpenMPT/soundlib/Load_dbm.cpp b/Frameworks/OpenMPT/OpenMPT/soundlib/Load_dbm.cpp index 4df9435e4..8fa92c422 100644 --- a/Frameworks/OpenMPT/OpenMPT/soundlib/Load_dbm.cpp +++ b/Frameworks/OpenMPT/OpenMPT/soundlib/Load_dbm.cpp @@ -109,7 +109,7 @@ struct DBMInstrument void ConvertToMPT(ModSample &mptSmp) const { - mptSmp.Initialize(); + mptSmp.Initialize(MOD_TYPE_DBM); mptSmp.nVolume = std::min(static_cast(volume), uint16(64)) * 4u; mptSmp.nC5Speed = Util::muldivr(sampleRate, 8303, 8363); @@ -583,7 +583,7 @@ bool CSoundFile::ReadDBM(FileReader &file, ModLoadingFlags loadFlags) cmd1 = CMD_NONE; } - const auto lostCommand = m.FillInTwoCommands(cmd1, param1, cmd2, param2); + const auto lostCommand = m.FillInTwoCommands(cmd1, param1, cmd2, param2, true); if(ModCommand::IsGlobalCommand(lostCommand.first, lostCommand.second)) lostGlobalCommands.insert(lostGlobalCommands.begin(), lostCommand); // Insert at front so that the last command of same type "wins" diff --git a/Frameworks/OpenMPT/OpenMPT/soundlib/Load_imf.cpp b/Frameworks/OpenMPT/OpenMPT/soundlib/Load_imf.cpp index 3a2c02a29..af779fcd7 100644 --- a/Frameworks/OpenMPT/OpenMPT/soundlib/Load_imf.cpp +++ b/Frameworks/OpenMPT/OpenMPT/soundlib/Load_imf.cpp @@ -177,7 +177,7 @@ struct IMFSample // Convert an IMFSample to OpenMPT's internal sample representation. void ConvertToMPT(ModSample &mptSmp) const { - mptSmp.Initialize(); + mptSmp.Initialize(MOD_TYPE_IMF); mptSmp.filename = mpt::String::ReadBuf(mpt::String::nullTerminated, filename); mptSmp.nLength = length; @@ -556,7 +556,7 @@ bool CSoundFile::ReadIMF(FileReader &file, ModLoadingFlags loadFlags) const auto [e1c, e1d, e2c, e2d] = patternChunk.ReadArray(); // Command 1, Data 1, Command 2, Data 2 const auto [command1, param1] = TranslateIMFEffect(e1c, e1d); const auto [command2, param2] = TranslateIMFEffect(e2c, e2d); - m.FillInTwoCommands(command1, param1, command2, param2); + m.FillInTwoCommands(command1, param1, command2, param2, true); } else if(mask & 0xC0) { // There's one effect, just stick it in the effect column (unless it's a volume command) diff --git a/Frameworks/OpenMPT/OpenMPT/soundlib/Load_it.cpp b/Frameworks/OpenMPT/OpenMPT/soundlib/Load_it.cpp index 277d11743..5edd4885f 100644 --- a/Frameworks/OpenMPT/OpenMPT/soundlib/Load_it.cpp +++ b/Frameworks/OpenMPT/OpenMPT/soundlib/Load_it.cpp @@ -798,6 +798,19 @@ bool CSoundFile::ReadIT(FileReader &file, ModLoadingFlags loadFlags) bool possibleXMconversion = false; + // There's a bug in IT somewhere that resets the "sample data present" flag in sample headers, but keeps the sample length + // of a previously deleted sample (presumably). + // As old ModPlug versions didn't set this flag under some circumstances (if a sample wasn't referenced by any instruments in instrument mode), + // and because there appear to be some external tools that forget to set this flag at all, we only respect the flag if the file + // vaguely looks like it was saved with IT. Some files that play garbage data if we don't do this: + // astral projection.it by Lord Jon Ray + // classic illusions.it by Blackstar + // deep in dance.it by Simply DJ + // There are many more such files but they don't reference the broken samples in their pattern data, or the sample data pointer + // points right to the end of the file, so in both cases no audible problem can be observed. + const bool muteBuggySamples = !interpretModPlugMade && fileHeader.cwtv >= 0x0100 && fileHeader.cwtv <= 0x0217 + && (fileHeader.cwtv < 0x0207 || fileHeader.reserved != 0); + // Reading Samples m_nSamples = std::min(static_cast(fileHeader.smpnum), static_cast(MAX_SAMPLES - 1)); bool lastSampleCompressed = false, anyADPCM = false; @@ -806,9 +819,10 @@ bool CSoundFile::ReadIT(FileReader &file, ModLoadingFlags loadFlags) 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); + if(muteBuggySamples && !(sampleHeader.flags & ITSample::sampleDataPresent)) + sample.nLength = 0; m_szNames[i + 1] = mpt::String::ReadBuf(mpt::String::spacePadded, sampleHeader.name); @@ -1257,6 +1271,10 @@ bool CSoundFile::ReadIT(FileReader &file, ModLoadingFlags loadFlags) } else if(fileHeader.cwtv == 0 && madeWithTracker.empty()) { madeWithTracker = U_("Unknown"); + } else if(fileHeader.cwtv >= 0x0208 && fileHeader.cwtv <= 0x0214 && !fileHeader.reserved && m_FileHistory.empty() && madeWithTracker.empty()) + { + // Any file made with IT starting from v2.07 onwards should have an edit history + madeWithTracker = UL_("Unknown"); } else if(fileHeader.cmwt < 0x0300 && madeWithTracker.empty()) { madeWithTracker = GetImpulseTrackerVersion(fileHeader.cwtv, fileHeader.cmwt); @@ -2650,14 +2668,6 @@ bool CSoundFile::LoadExtendedSongProperties(FileReader &file, bool ignoreChannel 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); return true; } diff --git a/Frameworks/OpenMPT/OpenMPT/soundlib/Load_med.cpp b/Frameworks/OpenMPT/OpenMPT/soundlib/Load_med.cpp index 9abf61a86..42361afe8 100644 --- a/Frameworks/OpenMPT/OpenMPT/soundlib/Load_med.cpp +++ b/Frameworks/OpenMPT/OpenMPT/soundlib/Load_med.cpp @@ -368,7 +368,7 @@ struct MMDDump MPT_BINARY_STRUCT(MMDDump, 10) -static TEMPO MMDTempoToBPM(uint32 tempo, bool is8Ch, bool bpmMode, uint8 rowsPerBeat) +static TEMPO MMDTempoToBPM(uint32 tempo, bool is8Ch, bool softwareMixing, bool bpmMode, uint8 rowsPerBeat) { if(bpmMode && !is8Ch) { @@ -382,10 +382,14 @@ static TEMPO MMDTempoToBPM(uint32 tempo, bool is8Ch, bool bpmMode, uint8 rowsPer // MED Soundstudio uses these tempos when importing old files static constexpr uint8 tempos[10] = {179, 164, 152, 141, 131, 123, 116, 110, 104, 99}; return TEMPO(tempos[tempo - 1], 0); - } else if(tempo > 0 && tempo <= 10) + } else if(!softwareMixing && tempo > 0 && tempo <= 10) { // SoundTracker compatible tempo return TEMPO((6.0 * 1773447.0 / 14500.0) / tempo); + } else if(softwareMixing && tempo < 8) + { + // Observed in MED SoundStudio 1.03 with 1-64ch mixing mode and SPD tempo mode (bug?) + return TEMPO(157.86); } return TEMPO(tempo / 0.264); @@ -398,10 +402,11 @@ struct TranslateMEDPatternContext const CHANNELINDEX numTracks; const uint8 version; const uint8 rowsPerBeat; - const bool hardwareMixSamples : 1; const bool is8Ch : 1; + const bool softwareMixing : 1; const bool bpmMode : 1; const bool volHex : 1; + const bool vol7bit : 1; }; @@ -410,9 +415,29 @@ static std::pair ConvertMEDEffect(ModCommand & const uint8 nibbleLo = std::min(param, uint8(0x0F)); switch(command) { + case 0x01: // Portamento Up (avoid effect memory when importing as XM) + if(param) + m.SetEffectCommand(CMD_PORTAMENTOUP, param); + break; + case 0x02: // Portamento Down (avoid effect memory when importing as XM) + if(param) + m.SetEffectCommand(CMD_PORTAMENTODOWN, param); + break; case 0x04: // Vibrato (twice as deep as in ProTracker) m.SetEffectCommand(CMD_VIBRATO, (param & 0xF0) | std::min((param & 0x0F) * 2, 0x0F)); break; + case 0x05: // Tone Porta + Volume Slide (avoid effect memory when importing as XM) + if(param) + m.SetEffectCommand(CMD_TONEPORTAVOL, param); + else + m.SetEffectCommand(CMD_TONEPORTAMENTO, 0); + break; + case 0x06: // Vibrato + Volume Slide (avoid effect memory when importing as XM) + if(param) + m.SetEffectCommand(CMD_VIBRATOVOL, param); + else + m.SetEffectCommand(CMD_VIBRATO, 0); + break; case 0x08: // Hold and decay break; case 0x09: // Set secondary speed @@ -422,13 +447,14 @@ static std::pair ConvertMEDEffect(ModCommand & case 0x0C: // Set Volume (note: parameters >= 0x80 (only in hex mode?) should set the default instrument volume, which we don't support) if(!ctx.volHex && param < 0x99) m.SetEffectCommand(CMD_VOLUME, static_cast((param >> 4) * 10 + (param & 0x0F))); - else if(ctx.volHex && ctx.version < 3) + else if(ctx.volHex && !ctx.vol7bit) m.SetEffectCommand(CMD_VOLUME, static_cast(std::min(param & 0x7F, 64))); else if(ctx.volHex) m.SetEffectCommand(CMD_VOLUME, static_cast(((param & 0x7F) + 1) / 2)); break; case 0x0D: - m.SetEffectCommand(CMD_VOLUMESLIDE, param); + if(param) + m.SetEffectCommand(CMD_VOLUMESLIDE, param); break; case 0x0E: // Synth jump m.command = CMD_NONE; @@ -446,7 +472,7 @@ static std::pair ConvertMEDEffect(ModCommand & m.param = 0x70; } else { - uint16 tempo = mpt::saturate_round(MMDTempoToBPM(param, ctx.is8Ch, ctx.bpmMode, ctx.rowsPerBeat).ToDouble()); + uint16 tempo = mpt::saturate_round(MMDTempoToBPM(param, ctx.is8Ch, ctx.softwareMixing, ctx.bpmMode, ctx.rowsPerBeat).ToDouble()); if(tempo <= Util::MaxValueOfType(m.param)) { m.param = static_cast(tempo); @@ -602,7 +628,7 @@ static bool TranslateMEDPattern(FileReader &file, FileReader &cmdExt, CPattern & param1 = param; } // Octave wrapping for 4-channel modules - if(ctx.hardwareMixSamples && note >= NOTE_MIDDLEC + 2 * 12) + if(note >= NOTE_MIDDLEC + 2 * 12) needInstruments = true; if(note >= NOTE_MIN && note <= NOTE_MAX) @@ -615,7 +641,7 @@ static bool TranslateMEDPattern(FileReader &file, FileReader &cmdExt, CPattern & if(oldCmd.first != CMD_NONE && m->command != oldCmd.first) { if(!ModCommand::CombineEffects(m->command, m->param, oldCmd.first, oldCmd.second) && m->volcmd == VOLCMD_NONE) - m->FillInTwoCommands(m->command, m->param, oldCmd.first, oldCmd.second); + m->FillInTwoCommands(m->command, m->param, oldCmd.first, oldCmd.second, true); // Reset X-Param to 8-bit value if this cell was overwritten with a "useful" effect if(row > 0 && oldCmd.first == CMD_XPARAM && m->command != CMD_XPARAM) pattern.GetpModCommand(row - 1, chn)->param = Util::MaxValueOfType(m->param); @@ -895,13 +921,14 @@ bool CSoundFile::ReadMED(FileReader &file, ModLoadingFlags loadFlags) { needInstruments = true; instr.Transpose(-24); - } else if(!isSynth && hardwareMixSamples) + } else if(!isSynth && (hardwareMixSamples || sampleHeader.sampleTranspose)) { + int offset = NOTE_MIDDLEC + (hardwareMixSamples ? 24 : 36); for(auto ¬e : instr.NoteMap) { int realNote = note + sampleHeader.sampleTranspose; - if(realNote >= NOTE_MIDDLEC + 24) - note -= static_cast(mpt::align_down(realNote - NOTE_MIDDLEC - 12, 12)); + if(realNote >= offset) + note -= static_cast(mpt::align_down(realNote - offset + 12, 12)); } } @@ -968,6 +995,7 @@ bool CSoundFile::ReadMED(FileReader &file, ModLoadingFlags loadFlags) { sampleIO |= SampleIO::stereoSplit; length /= 2; + m_SongFlags.reset(SONG_ISAMIGA); // Amiga resampler does not handle stereo samples } if(instrHeader.type & MMDInstrHeader::DELTA) { @@ -1167,9 +1195,9 @@ bool CSoundFile::ReadMED(FileReader &file, ModLoadingFlags loadFlags) if((header.mixEchoType == 1 || header.mixEchoType == 2) && numPlugins < MAX_MIXPLUGINS) { // Emulating MED echo using the DMO echo requires to compensate for the differences in initial feedback in the latter. - const float feedback = 1.0f / (1 << std::max(header.mixEchoDepth, uint8(1))); // The feedback we want - const float initialFeedback = std::sqrt(1.0f - (feedback * feedback)); // Actual strength of first delay's feedback - const float wetFactor = feedback / initialFeedback; // Factor to compensate for this + const float feedback = 1.0f / (1 << std::clamp(header.mixEchoDepth, uint8(1), uint8(9))); // The feedback we want + const float initialFeedback = std::sqrt(1.0f - (feedback * feedback)); // Actual strength of first delay's feedback + const float wetFactor = feedback / initialFeedback; // Factor to compensate for this const float delay = (std::max(header.mixEchoLength.get(), uint16(1)) - 1) / 1999.0f; SNDMIXPLUGIN &mixPlug = m_MixPlugins[numPlugins]; mpt::reconstruct(mixPlug); @@ -1257,10 +1285,11 @@ bool CSoundFile::ReadMED(FileReader &file, ModLoadingFlags loadFlags) const bool volHex = (songHeader.flags & MMDSong::FLAG_VOLHEX) != 0; const bool is8Ch = (songHeader.flags & MMDSong::FLAG_8CHANNEL) != 0; const bool bpmMode = (songHeader.flags2 & MMDSong::FLAG2_BPM) != 0; + const bool softwareMixing = (songHeader.flags2 & MMDSong::FLAG2_MIX) != 0; const uint8 rowsPerBeat = 1 + (songHeader.flags2 & MMDSong::FLAG2_BMASK); if(song == 0) { - m_nDefaultTempo = MMDTempoToBPM(songHeader.defaultTempo, is8Ch, bpmMode, rowsPerBeat); + m_nDefaultTempo = MMDTempoToBPM(songHeader.defaultTempo, is8Ch, softwareMixing, bpmMode, rowsPerBeat); m_nDefaultSpeed = Clamp(songHeader.tempo2, 1, 32); if(bpmMode) { @@ -1275,6 +1304,9 @@ bool CSoundFile::ReadMED(FileReader &file, ModLoadingFlags loadFlags) // For MED, this affects both volume and pitch slides m_SongFlags.set(SONG_FASTVOLSLIDES, !(songHeader.flags & MMDSong::FLAG_STSLIDE)); + m_playBehaviour.set(kST3OffsetWithoutInstrument); + m_playBehaviour.set(kST3PortaSampleChange); + m_playBehaviour.set(kFT2PortaNoNote); if(expData.songNameOffset && file.Seek(expData.songNameOffset)) { @@ -1382,6 +1414,7 @@ bool CSoundFile::ReadMED(FileReader &file, ModLoadingFlags loadFlags) int16 transpose = NOTE_MIN + 47 + songHeader.playTranspose; uint16 numPages = 0; FileReader cmdExt, commandPages; + bool vol7bit = false; if(version < 1) { @@ -1423,6 +1456,7 @@ bool CSoundFile::ReadMED(FileReader &file, ModLoadingFlags loadFlags) && file.Seek(blockInfo.cmdExtTableOffset) && file.Seek(file.ReadUint32BE())) { + vol7bit = true; cmdExt = file.ReadChunk(numTracks * numRows * (1 + numPages)); } @@ -1437,7 +1471,7 @@ bool CSoundFile::ReadMED(FileReader &file, ModLoadingFlags loadFlags) pattern.SetName(patName); LimitMax(numTracks, m_nChannels); - TranslateMEDPatternContext context{transpose, numTracks, version, rowsPerBeat, hardwareMixSamples, is8Ch, bpmMode, volHex}; + TranslateMEDPatternContext context{transpose, numTracks, version, rowsPerBeat, is8Ch, softwareMixing, bpmMode, volHex, vol7bit}; needInstruments |= TranslateMEDPattern(file, cmdExt, pattern, context, false); for(uint16 page = 0; page < numPages; page++) diff --git a/Frameworks/OpenMPT/OpenMPT/soundlib/Load_mod.cpp b/Frameworks/OpenMPT/OpenMPT/soundlib/Load_mod.cpp index b592573df..a53aa79d4 100644 --- a/Frameworks/OpenMPT/OpenMPT/soundlib/Load_mod.cpp +++ b/Frameworks/OpenMPT/OpenMPT/soundlib/Load_mod.cpp @@ -617,8 +617,12 @@ static PATTERNINDEX GetNumPatterns(FileReader &file, ModSequence &Order, ORDERIN const size_t patternStartOffset = file.GetPosition(); const size_t sizeWithoutPatterns = totalSampleLen + patternStartOffset; const size_t sizeWithOfficialPatterns = sizeWithoutPatterns + officialPatterns * numChannels * 256; + // There are some WOW files with an extra byte at the end, and also a MOD file (idntmind.mod, MD5 a3af5c3e1af269e32dfb6677c41c8453, SHA1 4884717c298575f9884b2211c762bb1725f73743) + // where only the "official" patterns should be counted but the file also has an extra byte at the end. + // Since MOD files can technically not have an odd file size, we just always round the actual file size down. + const auto fileSize = mpt::align_down(file.GetLength(), FileReader::pos_type{2}); - if(wowSampleLen && (wowSampleLen + patternStartOffset) + numPatterns * 8 * 256 == (file.GetLength() & ~1)) + if(wowSampleLen && (wowSampleLen + patternStartOffset) + numPatterns * 8 * 256 == fileSize) { // Check if this is a Mod's Grave WOW file... WOW files use the M.K. magic but are actually 8CHN files. // We do a simple pattern validation as well for regular MOD files that have non-module data attached at the end @@ -627,7 +631,7 @@ static PATTERNINDEX GetNumPatterns(FileReader &file, ModSequence &Order, ORDERIN if(ValidateMODPatternData(file, 16, true)) numChannels = 8; file.Seek(patternStartOffset); - } else if(numPatterns != officialPatterns && (validateHiddenPatterns || sizeWithOfficialPatterns == file.GetLength())) + } else if(numPatterns != officialPatterns && (validateHiddenPatterns || sizeWithOfficialPatterns == fileSize)) { // 15-sample SoundTracker specifics: // Fix SoundTracker modules where "hidden" patterns should be ignored. @@ -657,7 +661,7 @@ static PATTERNINDEX GetNumPatterns(FileReader &file, ModSequence &Order, ORDERIN file.Seek(patternStartOffset); } - if(numPatternsIllegal > numPatterns && sizeWithoutPatterns + numPatternsIllegal * numChannels * 256 == file.GetLength()) + if(numPatternsIllegal > numPatterns && sizeWithoutPatterns + numPatternsIllegal * numChannels * 256 == fileSize) { // Even those illegal pattern indexes (> 128) appear to be valid... What a weird file! // e.g. NIETNU.MOD, where the end of the order list is filled with FF rather than 00, and the file actually contains 256 patterns. @@ -1072,6 +1076,7 @@ bool CSoundFile::ReadMOD(FileReader &file, ModLoadingFlags loadFlags) // Reading patterns Patterns.ResizeArray(numPatterns); + std::bitset<32> referencedSamples; for(PATTERNINDEX pat = 0; pat < numPatterns; pat++) { ModCommand *rowBase = nullptr; @@ -1179,6 +1184,8 @@ bool CSoundFile::ReadMOD(FileReader &file, ModLoadingFlags loadFlags) if(m.instr != 0) { lastInstrument[chn] = m.instr; + if(isStartrekker) + referencedSamples.set(m.instr & 0x1F); } } if(hasSpeedOnRow && hasTempoOnRow) @@ -1215,7 +1222,7 @@ bool CSoundFile::ReadMOD(FileReader &file, ModLoadingFlags loadFlags) { m_SongFlags.set(SONG_ISAMIGA); } - if(isGenericMultiChannel || isMdKd) + if(isGenericMultiChannel || isMdKd || IsMagic(magic, "M!K!")) { m_playBehaviour.set(kFT2MODTremoloRampWaveform); } @@ -1316,7 +1323,7 @@ bool CSoundFile::ReadMOD(FileReader &file, ModLoadingFlags loadFlags) m_nInstruments = 31; #endif - for(SAMPLEINDEX smp = 1; smp <= m_nInstruments; smp++) + for(SAMPLEINDEX smp = 1; smp <= GetNumInstruments(); smp++) { // For Startrekker AM synthesis, we need instrument envelopes. ModInstrument *ins = AllocateInstrument(smp, smp); @@ -1339,6 +1346,32 @@ bool CSoundFile::ReadMOD(FileReader &file, ModLoadingFlags loadFlags) } #endif // MPT_EXTERNAL_SAMPLES || MPT_BUILD_FUZZER + if((loadFlags & loadSampleData) && isStartrekker && !m_nInstruments) + { + uint8 emptySampleReferences = 0; + for(SAMPLEINDEX smp = 1; smp <= 31; smp++) + { + if(referencedSamples[smp] && !Samples[smp].nLength) + { + if(++emptySampleReferences > 1) + { +#ifdef MPT_EXTERNAL_SAMPLES + mpt::ustring filenameHint; + if(file.GetOptionalFileName()) + { + const auto filename = file.GetOptionalFileName()->GetFilename().ToUnicode(); + filenameHint = MPT_UFORMAT(" ({}.nt or {}.as)")(filename, filename); + } + AddToLog(LogWarning, MPT_UFORMAT("This Startrekker AM file is most likely missing its companion file{}. Synthesized instruments will not play.")(filenameHint)); +#else + AddToLog(LogWarning, U_("This appears to be a Startrekker AM file with external synthesizes instruments. External instruments are currently not supported.")); +#endif // MPT_EXTERNAL_SAMPLES + break; + } + } + } + } + // Fix VBlank MODs. Arbitrary threshold: 8 minutes (enough for "frame of mind" by Dascon...). // Basically, this just converts all tempo commands into speed commands // for MODs which are supposed to have VBlank timing (instead of CIA timing). diff --git a/Frameworks/OpenMPT/OpenMPT/soundlib/Load_s3m.cpp b/Frameworks/OpenMPT/OpenMPT/soundlib/Load_s3m.cpp index 3b912b64b..c4fce463c 100644 --- a/Frameworks/OpenMPT/OpenMPT/soundlib/Load_s3m.cpp +++ b/Frameworks/OpenMPT/OpenMPT/soundlib/Load_s3m.cpp @@ -232,6 +232,15 @@ bool CSoundFile::ReadS3M(FileReader &file, ModLoadingFlags loadFlags) m_nMinPeriod = 64; m_nMaxPeriod = 32767; + ReadOrderFromFile(Order(), file, fileHeader.ordNum, 0xFF, 0xFE); + + // Read sample header offsets + std::vector sampleOffsets; + file.ReadVector(sampleOffsets, fileHeader.smpNum); + // Read pattern offsets + std::vector patternOffsets; + file.ReadVector(patternOffsets, fileHeader.patNum); + // ST3 ignored Zxx commands, so if we find that a file was made with ST3, we should erase all MIDI macros. bool keepMidiMacros = false; @@ -241,6 +250,7 @@ bool CSoundFile::ReadS3M(FileReader &file, ModLoadingFlags loadFlags) bool isST3 = false; bool isSchism = false; const bool usePanningTable = fileHeader.usePanningTable == S3MFileHeader::idPanning; + const bool offsetsAreCanonical = !patternOffsets.empty() && !sampleOffsets.empty() && patternOffsets[0] > sampleOffsets[0]; const int32 schismDateVersion = SchismTrackerEpoch + ((fileHeader.cwtv == 0x4FFF) ? fileHeader.reserved2 : (fileHeader.cwtv - 0x4050)); switch(fileHeader.cwtv & S3MFileHeader::trackerMask) { @@ -252,18 +262,25 @@ bool CSoundFile::ReadS3M(FileReader &file, ModLoadingFlags loadFlags) if(!memcmp(&fileHeader.reserved2, "SCLUB2.0", 8)) { madeWithTracker = UL_("Sound Club 2"); - } else if(fileHeader.cwtv == S3MFileHeader::trkST3_20 && fileHeader.special == 0 && (fileHeader.ordNum & 0x0F) == 0 && fileHeader.ultraClicks == 0 && (fileHeader.flags & ~0x50) == 0 && usePanningTable) + } else if(fileHeader.cwtv == S3MFileHeader::trkST3_20 && fileHeader.special == 0 && (fileHeader.ordNum & 0x01) == 0 && fileHeader.ultraClicks == 0 && (fileHeader.flags & ~0x50) == 0 && usePanningTable && offsetsAreCanonical) { - // MPT and OpenMPT before 1.17.03.02 - Simply keep default (filter) MIDI macros - if((fileHeader.masterVolume & 0x80) != 0) + // Canonical offset check avoids mis-detection of an automatic conversion of Vic's "Paper" demo track + if((fileHeader.ordNum & 0x0F) == 0) { - m_dwLastSavedWithVersion = MPT_V("1.16"); - madeWithTracker = UL_("ModPlug Tracker / OpenMPT 1.17"); - } else + // MPT and OpenMPT before 1.17.03.02 - Simply keep default (filter) MIDI macros + if((fileHeader.masterVolume & 0x80) != 0) + { + m_dwLastSavedWithVersion = MPT_V("1.16"); + madeWithTracker = UL_("ModPlug Tracker / OpenMPT 1.17"); + } else + { + // MPT 1.0 alpha5 doesn't set the stereo flag, but MPT 1.0 alpha6 does. + m_dwLastSavedWithVersion = MPT_V("1.00.00.A0"); + madeWithTracker = UL_("ModPlug Tracker 1.0 alpha"); + } + } else if((fileHeader.masterVolume & 0x80) != 0) { - // MPT 1.0 alpha5 doesn't set the stereo flag, but MPT 1.0 alpha6 does. - m_dwLastSavedWithVersion = MPT_V("1.00.00.A0"); - madeWithTracker = UL_("ModPlug Tracker 1.0 alpha"); + madeWithTracker = UL_("Schism Tracker"); } keepMidiMacros = true; nonCompatTracker = true; @@ -511,15 +528,6 @@ bool CSoundFile::ReadS3M(FileReader &file, ModLoadingFlags loadFlags) m_nChannels = 1; } - ReadOrderFromFile(Order(), file, fileHeader.ordNum, 0xFF, 0xFE); - - // Read sample header offsets - std::vector sampleOffsets; - file.ReadVector(sampleOffsets, fileHeader.smpNum); - // Read pattern offsets - std::vector patternOffsets; - file.ReadVector(patternOffsets, fileHeader.patNum); - // Read extended channel panning if(usePanningTable) { @@ -538,6 +546,7 @@ bool CSoundFile::ReadS3M(FileReader &file, ModLoadingFlags loadFlags) if(m_nChannels < 32 && m_dwLastSavedWithVersion == MPT_V("1.16")) { // MPT 1.0 alpha 6 up to 1.16.203 set ths panning bit for all channels, regardless of whether they are used or not. + // Note: Schism Tracker fixed the same bug in git commit f21fe8bcae8b6dde2df27ede4ac9fe563f91baff if(hasChannelsWithoutPanning) m_modFormat.madeWithTracker = UL_("ModPlug Tracker 1.16 / OpenMPT 1.17"); else diff --git a/Frameworks/OpenMPT/OpenMPT/soundlib/Load_stm.cpp b/Frameworks/OpenMPT/OpenMPT/soundlib/Load_stm.cpp index b6bf9dc1a..b158e91f4 100644 --- a/Frameworks/OpenMPT/OpenMPT/soundlib/Load_stm.cpp +++ b/Frameworks/OpenMPT/OpenMPT/soundlib/Load_stm.cpp @@ -49,7 +49,7 @@ struct STMSampleHeader && mptSmp.nLoopEnd != 0xFFFF) { mptSmp.uFlags = CHN_LOOP; - mptSmp.nLoopEnd = std::min(mptSmp.nLoopEnd, mptSmp.nLength); + mptSmp.nLength = std::max(mptSmp.nLoopEnd, mptSmp.nLength); } } }; diff --git a/Frameworks/OpenMPT/OpenMPT/soundlib/ModSample.cpp b/Frameworks/OpenMPT/OpenMPT/soundlib/ModSample.cpp index e3779f9ba..e59757ac7 100644 --- a/Frameworks/OpenMPT/OpenMPT/soundlib/ModSample.cpp +++ b/Frameworks/OpenMPT/OpenMPT/soundlib/ModSample.cpp @@ -41,6 +41,13 @@ void ModSample::Convert(MODTYPE fromType, MODTYPE toType) nC5Speed = Util::muldivr_unsigned(nC5Speed, 8363, 8287); FrequencyToTranspose(); } + if(toType == MOD_TYPE_MOD) + { + if(RelativeTone == -1 && nFineTune == 0) + nFineTune = -128; + RelativeTone = 0; + nFineTune &= ~0x0F; + } // No ping-pong loop, panning and auto-vibrato for MOD / S3M samples if(toType & (MOD_TYPE_MOD | MOD_TYPE_S3M)) @@ -51,8 +58,6 @@ void ModSample::Convert(MODTYPE fromType, MODTYPE toType) nVibRate = 0; nVibSweep = 0; nVibType = VIB_SINE; - - RelativeTone = 0; } // No global volume / sustain loops for MOD/S3M/XM @@ -85,7 +90,6 @@ void ModSample::Convert(MODTYPE fromType, MODTYPE toType) LimitMax(nVibRate, uint8(63)); } - // Autovibrato sweep setting is inverse in XM (0 = "no sweep") and IT (0 = "no vibrato") if(((fromType & MOD_TYPE_XM) && (toType & (MOD_TYPE_IT | MOD_TYPE_MPT))) || ((toType & MOD_TYPE_XM) && (fromType & (MOD_TYPE_IT | MOD_TYPE_MPT)))) { @@ -150,7 +154,16 @@ void ModSample::Initialize(MODTYPE type) rootNote = 0; filename = ""; - RemoveAllCuePoints(); + if(type & (MOD_TYPE_DBM | MOD_TYPE_IMF | MOD_TYPE_MED)) + { + for(SmpLength i = 1; i < 10; i++) + { + cues[i - 1] = Util::muldiv_unsigned(i, 255 * 256, 9); + } + } else + { + RemoveAllCuePoints(); + } } diff --git a/Frameworks/OpenMPT/OpenMPT/soundlib/SampleFormatFLAC.cpp b/Frameworks/OpenMPT/OpenMPT/soundlib/SampleFormatFLAC.cpp index d7d2a3a96..b296d35bb 100644 --- a/Frameworks/OpenMPT/OpenMPT/soundlib/SampleFormatFLAC.cpp +++ b/Frameworks/OpenMPT/OpenMPT/soundlib/SampleFormatFLAC.cpp @@ -558,7 +558,7 @@ bool CSoundFile::SaveFLACSample(SAMPLEINDEX nSample, std::ostream &f) const { #ifdef MPT_WITH_FLAC const ModSample &sample = Samples[nSample]; - if(sample.uFlags[CHN_ADLIB]) + if(sample.uFlags[CHN_ADLIB] || !sample.HasSampleData()) return false; FLAC__StreamEncoder_RAII encoder(f); diff --git a/Frameworks/OpenMPT/OpenMPT/soundlib/SampleFormats.cpp b/Frameworks/OpenMPT/OpenMPT/soundlib/SampleFormats.cpp index 7df686790..1a02584b1 100644 --- a/Frameworks/OpenMPT/OpenMPT/soundlib/SampleFormats.cpp +++ b/Frameworks/OpenMPT/OpenMPT/soundlib/SampleFormats.cpp @@ -559,7 +559,7 @@ bool CSoundFile::ReadWAVSample(SAMPLEINDEX nSample, FileReader &file, bool mayNo bool CSoundFile::SaveWAVSample(SAMPLEINDEX nSample, std::ostream &f) const { const ModSample &sample = Samples[nSample]; - if(sample.uFlags[CHN_ADLIB]) + if(sample.uFlags[CHN_ADLIB] || !sample.HasSampleData()) return false; mpt::IO::OFile ff(f); @@ -843,6 +843,8 @@ bool CSoundFile::ReadW64Sample(SAMPLEINDEX nSample, FileReader &file, bool mayNo bool CSoundFile::SaveRAWSample(SAMPLEINDEX nSample, std::ostream &f) const { const ModSample &sample = Samples[nSample]; + if(!sample.HasSampleData()) + return false; SampleIO( sample.uFlags[CHN_16BIT] ? SampleIO::_16bit : SampleIO::_8bit, sample.uFlags[CHN_STEREO] ? SampleIO::stereoInterleaved : SampleIO::mono, @@ -1188,6 +1190,8 @@ bool CSoundFile::ReadS3ISample(SAMPLEINDEX nSample, FileReader &file) bool CSoundFile::SaveS3ISample(SAMPLEINDEX smp, std::ostream &f) const { const ModSample &sample = Samples[smp]; + if(!sample.uFlags[CHN_ADLIB] && !sample.HasSampleData()) + return false; S3MSampleHeader sampleHeader{}; SmpLength length = sampleHeader.ConvertToS3M(sample); mpt::String::WriteBuf(mpt::String::nullTerminated, sampleHeader.name) = m_szNames[smp]; @@ -2762,7 +2766,7 @@ static uint32 WriteIFFStringChunk(std::ostream &f, IFFChunk::ChunkIdentifiers id bool CSoundFile::SaveIFFSample(SAMPLEINDEX smp, std::ostream &f) const { const ModSample &sample = Samples[smp]; - if(sample.uFlags[CHN_ADLIB]) + if(sample.uFlags[CHN_ADLIB] || !sample.HasSampleData()) return false; mpt::IO::OFile ff(f); diff --git a/Frameworks/OpenMPT/OpenMPT/soundlib/Snd_defs.h b/Frameworks/OpenMPT/OpenMPT/soundlib/Snd_defs.h index 45a3de7fb..61c767f9e 100644 --- a/Frameworks/OpenMPT/OpenMPT/soundlib/Snd_defs.h +++ b/Frameworks/OpenMPT/OpenMPT/soundlib/Snd_defs.h @@ -299,7 +299,8 @@ DECLARE_FLAGSET(SongFlags) #define SNDMIX_MUTECHNMODE 0x100000 // Notes are not played on muted channels -#define MAX_GLOBAL_VOLUME 256u +inline constexpr uint32 MAX_GLOBAL_VOLUME = 256; +inline constexpr uint32 MAX_PREAMP = 2000; // Resampling modes enum ResamplingMode : uint8 diff --git a/Frameworks/OpenMPT/OpenMPT/soundlib/Snd_fx.cpp b/Frameworks/OpenMPT/OpenMPT/soundlib/Snd_fx.cpp index 25455fb2f..effedbc1a 100644 --- a/Frameworks/OpenMPT/OpenMPT/soundlib/Snd_fx.cpp +++ b/Frameworks/OpenMPT/OpenMPT/soundlib/Snd_fx.cpp @@ -1391,7 +1391,7 @@ void CSoundFile::InstrumentChange(ModChannel &chn, uint32 instr, bool bPorta, bo if(pIns->NoteMap[note - NOTE_MIN] > NOTE_MAX) return; uint32 n = pIns->Keyboard[note - NOTE_MIN]; - pSmp = ((n) && (n < MAX_SAMPLES)) ? &Samples[n] : nullptr; + pSmp = (n <= GetNumSamples()) ? &Samples[n] : &Samples[0]; } else if(GetNumInstruments()) { // No valid instrument, or not a valid note. @@ -1751,9 +1751,9 @@ void CSoundFile::NoteChange(ModChannel &chn, int note, bool bPorta, bool bResetE if((pIns) && (note - NOTE_MIN < (int)std::size(pIns->Keyboard))) { uint32 n = pIns->Keyboard[note - NOTE_MIN]; - if((n) && (n < MAX_SAMPLES)) + if(n > 0) { - pSmp = &Samples[n]; + pSmp = &Samples[(n <= GetNumSamples()) ? n : 0]; } else if(m_playBehaviour[kITEmptyNoteMapSlot] && !chn.HasMIDIOutput()) { // Impulse Tracker ignores empty slots. @@ -1958,7 +1958,7 @@ void CSoundFile::NoteChange(ModChannel &chn, int note, bool bPorta, bool bResetE chn.nLoopEnd = pSmp->nLength; chn.nLoopStart = 0; chn.position.Set(0); - if((m_SongFlags[SONG_PT_MODE] || m_playBehaviour[kST3OffsetWithoutInstrument]) && !chn.rowCommand.instr) + if((m_SongFlags[SONG_PT_MODE] || m_playBehaviour[kST3OffsetWithoutInstrument] || GetType() == MOD_TYPE_MED) && !chn.rowCommand.instr) { chn.position.SetInt(std::min(chn.prevNoteOffset, chn.nLength - SmpLength(1))); } else @@ -2229,9 +2229,9 @@ CHANNELINDEX CSoundFile::CheckNNA(CHANNELINDEX nChn, uint32 instr, int note, boo // Test case: dct_smp_note_test.it if(!m_playBehaviour[kITDCTBehaviour] || !m_playBehaviour[kITRealNoteMapping]) dnaNote = pIns->NoteMap[note - NOTE_MIN]; - if(smp > 0 && smp < MAX_SAMPLES) + if(smp > 0) { - pSample = &Samples[smp]; + pSample = &Samples[(smp <= GetNumSamples()) ? smp : 0]; } else if(m_playBehaviour[kITEmptyNoteMapSlot] && !pIns->HasValidMIDIChannel()) { // Impulse Tracker ignores empty slots. @@ -4993,6 +4993,7 @@ void CSoundFile::ProcessMIDIMacro(PlayState &playState, CHANNELINDEX nChn, bool } else { // SysEx message, find end of message + sendLen = outSize - sendPos; for(uint32 i = sendPos + 1; i < outSize; i++) { if(out[i] == 0xF7) @@ -5002,12 +5003,6 @@ void CSoundFile::ProcessMIDIMacro(PlayState &playState, CHANNELINDEX nChn, bool break; } } - if(sendLen == 0) - { - // Didn't find end, so "invent" end of SysEx message - out[outSize++] = 0xF7; - sendLen = outSize - sendPos; - } } } else if(!(out[sendPos] & 0x80)) { @@ -5100,16 +5095,16 @@ void CSoundFile::ParseMIDIMacro(PlayState &playState, CHANNELINDEX nChn, bool is // Velocity // This is "almost" how IT does it - apparently, IT seems to lag one row behind on global volume or channel volume changes. const int swing = (m_playBehaviour[kITSwingBehaviour] || m_playBehaviour[kMPTOldSwingBehaviour]) ? chn.nVolSwing : 0; - const int vol = Util::muldiv((chn.nVolume + swing) * m_PlayState.m_nGlobalVolume, chn.nGlobalVol * chn.nInsVol, 1 << 20); + const int vol = Util::muldiv((chn.nVolume + swing) * playState.m_nGlobalVolume, chn.nGlobalVol * chn.nInsVol, 1 << 20); data = static_cast(Clamp(vol / 2, 1, 127)); - //data = (unsigned char)std::min((chn.nVolume * chn.nGlobalVol * m_nGlobalVolume) >> (1 + 6 + 8), 127); + //data = (unsigned char)std::min((chn.nVolume * chn.nGlobalVol * playState.m_nGlobalVolume) >> (1 + 6 + 8), 127); } else if(macro[pos] == 'u') { // Calculated volume // Same note as with velocity applies here, but apparently also for instrument / sample volumes? - const int vol = Util::muldiv(chn.nCalcVolume * m_PlayState.m_nGlobalVolume, chn.nGlobalVol * chn.nInsVol, 1 << 26); + const int vol = Util::muldiv(chn.nCalcVolume * playState.m_nGlobalVolume, chn.nGlobalVol * chn.nInsVol, 1 << 26); data = static_cast(Clamp(vol / 2, 1, 127)); - //data = (unsigned char)std::min((chn.nCalcVolume * chn.nGlobalVol * m_nGlobalVolume) >> (7 + 6 + 8), 127); + //data = (unsigned char)std::min((chn.nCalcVolume * chn.nGlobalVol * playState.m_nGlobalVolume) >> (7 + 6 + 8), 127); } else if(macro[pos] == 'x') { // Pan set @@ -5214,14 +5209,32 @@ void CSoundFile::ParseMIDIMacro(PlayState &playState, CHANNELINDEX nChn, bool is firstNibble = true; } } + // Finish current byte if(!firstNibble) - { - // Finish current byte outPos++; - } if(updateZxxParam < 0x80) chn.lastZxxParam = updateZxxParam; + // Add end of SysEx byte if necessary + for(size_t i = 0; i < outPos; i++) + { + if(out[i] != 0xF0) + continue; + if(outPos - i >= 4 && (out[i + 1] == 0xF0 || out[i + 1] == 0xF1)) + { + // Internal message + i += 3; + } else + { + // Real SysEx + while(i < outPos && out[i] != 0xF7) + i++; + if(i == outPos && outPos < out.size()) + out[outPos++] = 0xF7; + } + + } + out = out.first(outPos); } @@ -5438,7 +5451,7 @@ void CSoundFile::SampleOffset(ModChannel &chn, SmpLength param) const { // ST3 compatibility: Instrument-less note recalls previous note's offset // Test case: OxxMemory.s3m - if(m_playBehaviour[kST3OffsetWithoutInstrument]) + if(m_playBehaviour[kST3OffsetWithoutInstrument] || GetType() == MOD_TYPE_MED) chn.prevNoteOffset = 0; chn.prevNoteOffset += param; @@ -5717,7 +5730,10 @@ void CSoundFile::RetrigNote(CHANNELINDEX nChn, int param, int offset) const bool fading = chn.dwFlags[CHN_NOTEFADE]; const auto oldPrevNoteOffset = chn.prevNoteOffset; - chn.prevNoteOffset = 0; // Retriggered notes should not use previous offset (test case: OxxMemoryWithRetrig.s3m) + // Retriggered notes should not use previous offset in S3M + // Test cases: OxxMemoryWithRetrig.s3m, PTOffsetRetrigger.mod + if(GetType() == MOD_TYPE_S3M) + chn.prevNoteOffset = 0; // IT compatibility: Really weird combination of envelopes and retrigger (see Storlek's q.it testcase) // Test cases: retrig.it, RetrigSlide.s3m const bool itS3Mstyle = m_playBehaviour[kITRetrigger] || (GetType() == MOD_TYPE_S3M && chn.nLength && !oplRealRetrig); diff --git a/Frameworks/OpenMPT/OpenMPT/soundlib/Sndfile.cpp b/Frameworks/OpenMPT/OpenMPT/soundlib/Sndfile.cpp index dd722f97b..9901819bf 100644 --- a/Frameworks/OpenMPT/OpenMPT/soundlib/Sndfile.cpp +++ b/Frameworks/OpenMPT/OpenMPT/soundlib/Sndfile.cpp @@ -594,6 +594,8 @@ bool CSoundFile::CreateInternal(FileReader file, ModLoadingFlags loadFlags) for(SAMPLEINDEX nSmp = 1; nSmp <= m_nSamples; nSmp++) { ModSample &sample = Samples[nSmp]; + LimitMax(sample.nLength, MAX_SAMPLE_LENGTH); + sample.SanitizeLoops(); #ifdef MPT_EXTERNAL_SAMPLES if(SampleHasPath(nSmp)) @@ -662,6 +664,8 @@ bool CSoundFile::CreateInternal(FileReader file, ModLoadingFlags loadFlags) LimitMax(m_nDefaultRowsPerBeat, MAX_ROWS_PER_BEAT); LimitMax(m_nDefaultRowsPerMeasure, MAX_ROWS_PER_BEAT); LimitMax(m_nDefaultGlobalVolume, MAX_GLOBAL_VOLUME); + LimitMax(m_nSamplePreAmp, MAX_PREAMP); + LimitMax(m_nVSTiVolume, MAX_PREAMP); if(!m_tempoSwing.empty()) m_tempoSwing.resize(m_nDefaultRowsPerBeat); diff --git a/Frameworks/OpenMPT/OpenMPT/soundlib/Sndmix.cpp b/Frameworks/OpenMPT/OpenMPT/soundlib/Sndmix.cpp index 29f8f4a1e..147d0c795 100644 --- a/Frameworks/OpenMPT/OpenMPT/soundlib/Sndmix.cpp +++ b/Frameworks/OpenMPT/OpenMPT/soundlib/Sndmix.cpp @@ -1840,8 +1840,15 @@ void CSoundFile::ProcessSampleAutoVibrato(ModChannel &chn, int32 &period, Tuning vdelta += Util::muldiv(period, fineUpTable[l & 0x03], 0x10000) - period; } } - period = (period + vdelta) / 256; - nPeriodFrac = vdelta & 0xFF; + if(Util::MaxValueOfType(period) - period >= vdelta) + { + period = (period + vdelta) / 256; + nPeriodFrac = vdelta & 0xFF; + } else + { + period = Util::MaxValueOfType(period) / 256; + nPeriodFrac = 0; + } } else { // MPT's autovibrato code diff --git a/Frameworks/OpenMPT/OpenMPT/soundlib/modcommand.cpp b/Frameworks/OpenMPT/OpenMPT/soundlib/modcommand.cpp index dd987a45f..002d0ed10 100644 --- a/Frameworks/OpenMPT/OpenMPT/soundlib/modcommand.cpp +++ b/Frameworks/OpenMPT/OpenMPT/soundlib/modcommand.cpp @@ -1346,7 +1346,7 @@ bool ModCommand::CombineEffects(EffectCommand &eff1, uint8 ¶m1, EffectComman } -std::pair ModCommand::FillInTwoCommands(EffectCommand effect1, uint8 param1, EffectCommand effect2, uint8 param2) +std::pair ModCommand::FillInTwoCommands(EffectCommand effect1, uint8 param1, EffectCommand effect2, uint8 param2, bool allowLowResOffset) { if(effect1 == effect2) { @@ -1401,6 +1401,12 @@ std::pair ModCommand::FillInTwoCommands(Effect std::swap(effect1, effect2); std::swap(param1, param2); } + if(effect2 == CMD_OFFSET && (allowLowResOffset || param2 == 0)) + { + SetVolumeCommand(VOLCMD_OFFSET, static_cast(param2 ? std::max(param2 * 9 / 255, 1) : 0)); + SetEffectCommand(effect1, param1); + return {CMD_NONE, ModCommand::PARAM(0)}; + } SetVolumeCommand(VOLCMD_NONE, 0); SetEffectCommand(effect2, param2); return {effect1, param1}; diff --git a/Frameworks/OpenMPT/OpenMPT/soundlib/modcommand.h b/Frameworks/OpenMPT/OpenMPT/soundlib/modcommand.h index b8d248f13..ca360ee35 100644 --- a/Frameworks/OpenMPT/OpenMPT/soundlib/modcommand.h +++ b/Frameworks/OpenMPT/OpenMPT/soundlib/modcommand.h @@ -230,7 +230,7 @@ public: // Try to convert a an effect into a volume column effect. Returns converted effect on success. [[nodiscard]] static std::pair ConvertToVolCommand(const EffectCommand effect, PARAM param, bool force); // Takes two "normal" effect commands and converts them to volume column + effect column commands. Returns the dropped command + param (CMD_NONE if nothing had to be dropped). - std::pair FillInTwoCommands(EffectCommand effect1, uint8 param1, EffectCommand effect2, uint8 param2); + std::pair FillInTwoCommands(EffectCommand effect1, uint8 param1, EffectCommand effect2, uint8 param2, bool allowLowResOffset = false); // Try to combine two commands into one. Returns true on success and the combined command is placed in eff1 / param1. static bool CombineEffects(EffectCommand &eff1, uint8 ¶m1, EffectCommand &eff2, uint8 ¶m2); diff --git a/Frameworks/OpenMPT/OpenMPT/soundlib/plugins/dmo/I3DL2Reverb.cpp b/Frameworks/OpenMPT/OpenMPT/soundlib/plugins/dmo/I3DL2Reverb.cpp index 30d69f7ca..a566b87c2 100644 --- a/Frameworks/OpenMPT/OpenMPT/soundlib/plugins/dmo/I3DL2Reverb.cpp +++ b/Frameworks/OpenMPT/OpenMPT/soundlib/plugins/dmo/I3DL2Reverb.cpp @@ -505,7 +505,7 @@ void I3DL2Reverb::RecalculateI3DL2ReverbParams() m_roomFilter = 0.0f; } else { - float freq = std::cos(HFReference() * (2.0f * mpt::numbers::pi_v) / m_effectiveSampleRate); + float freq = std::min(std::cos(HFReference() * (2.0f * mpt::numbers::pi_v) / m_effectiveSampleRate), 0.9999f); float roomFilter = (freq * (roomHF + roomHF) - 2.0f + std::sqrt(freq * (roomHF * roomHF * freq * 4.0f) + roomHF * 8.0f - roomHF * roomHF * 4.0f - roomHF * freq * 8.0f)) / (roomHF + roomHF - 2.0f); m_roomFilter = Clamp(roomFilter, 0.0f, 1.0f); } diff --git a/Frameworks/OpenMPT/OpenMPT/soundlib/tuning.h b/Frameworks/OpenMPT/OpenMPT/soundlib/tuning.h index 2f4fa7888..118243e99 100644 --- a/Frameworks/OpenMPT/OpenMPT/soundlib/tuning.h +++ b/Frameworks/OpenMPT/OpenMPT/soundlib/tuning.h @@ -224,7 +224,8 @@ private: static bool IsValidRatio(RATIOTYPE ratio) { - return (ratio > static_cast(0.0)); + // Arbitrary epsilon > 0 to avoid NaNs and infinite values in ratio calculation + return (ratio > static_cast(0.02f)); } private: diff --git a/Frameworks/OpenMPT/OpenMPT/src/mpt/base/detect_compiler.hpp b/Frameworks/OpenMPT/OpenMPT/src/mpt/base/detect_compiler.hpp index ef1b73af5..e5613bf17 100644 --- a/Frameworks/OpenMPT/OpenMPT/src/mpt/base/detect_compiler.hpp +++ b/Frameworks/OpenMPT/OpenMPT/src/mpt/base/detect_compiler.hpp @@ -50,7 +50,9 @@ #elif defined(_MSC_VER) #define MPT_COMPILER_MSVC 1 -#if (_MSC_VER >= 1941) +#if (_MSC_VER >= 1942) +#define MPT_COMPILER_MSVC_VERSION MPT_COMPILER_MAKE_VERSION2(2022, 12) +#elif (_MSC_VER >= 1941) #define MPT_COMPILER_MSVC_VERSION MPT_COMPILER_MAKE_VERSION2(2022, 11) #elif (_MSC_VER >= 1940) #define MPT_COMPILER_MSVC_VERSION MPT_COMPILER_MAKE_VERSION2(2022, 10) diff --git a/Frameworks/OpenMPT/OpenMPT/src/mpt/io_write/buffer.hpp b/Frameworks/OpenMPT/OpenMPT/src/mpt/io_write/buffer.hpp index d0bf93e42..7c95f478c 100644 --- a/Frameworks/OpenMPT/OpenMPT/src/mpt/io_write/buffer.hpp +++ b/Frameworks/OpenMPT/OpenMPT/src/mpt/io_write/buffer.hpp @@ -46,18 +46,17 @@ public: inline WriteBuffer(Tfile & f_, mpt::byte_span buffer_) : buffer(buffer_) , f(f_) { + return; } inline ~WriteBuffer() noexcept(false) { - if (!writeError) - { + if (!writeError) { FlushLocal(); } } public: inline Tfile & file() const { - if (IsDirty()) - { + if (IsDirty()) { FlushLocal(); } return f; @@ -84,30 +83,24 @@ public: } inline bool Write(mpt::const_byte_span data) { bool result = true; - for (std::size_t i = 0; i < data.size(); ++i) - { + for (std::size_t i = 0; i < data.size(); ++i) { buffer[size] = data[i]; size++; - if (IsFull()) - { + if (IsFull()) { FlushLocal(); } } return result; } inline void FlushLocal() { - if (IsClean()) - { + if (IsClean()) { return; } - try - { - if (!mpt::IO::WriteRaw(f, mpt::as_span(buffer.data(), size))) - { + try { + if (!mpt::IO::WriteRaw(f, mpt::as_span(buffer.data(), size))) { writeError = true; } - } catch (const std::exception &) - { + } catch (const std::exception &) { writeError = true; throw; } diff --git a/Frameworks/OpenMPT/OpenMPT/test/test.cpp b/Frameworks/OpenMPT/OpenMPT/test/test.cpp index 0452688e9..d1a2465bf 100644 --- a/Frameworks/OpenMPT/OpenMPT/test/test.cpp +++ b/Frameworks/OpenMPT/OpenMPT/test/test.cpp @@ -2405,6 +2405,8 @@ static MPT_NOINLINE void TestCharsets() VERIFY_EQUAL(mpt::RelativePathToAbsolute(P_("\\foo"), exePath), P_("C:\\foo")); VERIFY_EQUAL(mpt::AbsolutePathToRelative(P_("\\\\server\\path\\file"), exePath), P_("\\\\server\\path\\file")); VERIFY_EQUAL(mpt::RelativePathToAbsolute(P_("\\\\server\\path\\file"), exePath), P_("\\\\server\\path\\file")); + VERIFY_EQUAL(mpt::AbsolutePathToRelative(P_("C:\\OpenMPT"), mpt::PathString{}), P_("C:\\OpenMPT")); + VERIFY_EQUAL(mpt::RelativePathToAbsolute(P_("C:\\OpenMPT"), mpt::PathString{}), P_("C:\\OpenMPT")); #endif #ifdef MODPLUG_TRACKER